diff --git a/core.py b/core.py index bf9e6f5..3c1f601 100644 --- a/core.py +++ b/core.py @@ -1,10 +1,12 @@ +import importlib import sys +import threading + import telegram -import worker + import configloader import utils -import threading -import importlib +import worker language = configloader.config["Config"]["language"] strings = importlib.import_module("strings." + language) diff --git a/database.py b/database.py index 4bdbfe4..67225de 100644 --- a/database.py +++ b/database.py @@ -1,13 +1,17 @@ +import datetime +import importlib import typing -from sqlalchemy import create_engine, Column, ForeignKey, UniqueConstraint -from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary, DateTime, Boolean -from sqlalchemy.orm import sessionmaker, relationship, backref + +import requests +import telegram +from sqlalchemy import (BigInteger, Boolean, Column, DateTime, ForeignKey, + Integer, LargeBinary, String, Text, UniqueConstraint, + create_engine) from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import backref, relationship, sessionmaker + import configloader -import telegram -import requests import utils -import importlib language = configloader.config["Config"]["language"] strings = importlib.import_module("strings." + language) @@ -31,9 +35,10 @@ class User(TableDeclarativeBase): first_name = Column(String, nullable=False) last_name = Column(String) username = Column(String) + last_seen = Column(DateTime, default=datetime.datetime.utcnow) # Current wallet credit - credit = Column(Integer, nullable=False) + credit = Column(Integer, nullable=False, server_default=0) # Extra table parameters __tablename__ = "users" @@ -46,8 +51,6 @@ def __init__(self, telegram_chat: telegram.Chat, **kwargs): self.first_name = telegram_chat.first_name self.last_name = telegram_chat.last_name self.username = telegram_chat.username - # The starting wallet value is 0 - self.credit = 0 def __str__(self): """Describe the user in the best way possible given the available data.""" diff --git a/strings/en_US.py b/strings/en_US.py index f153e77..e9d9cc0 100644 --- a/strings/en_US.py +++ b/strings/en_US.py @@ -115,8 +115,13 @@ conversation_open_help_menu = "What kind of help do you need?" # Conversation: confirm promotion to admin -conversation_confirm_admin_promotion = "Are you sure you want to promote this user to 💼 Manager?\n" \ - "It is an irreversible action!" +conversation_confirm_admin_promotion = "Are you sure you want to promote this user to 💼 Manager?\n" + +# Conversation: remove administrator +conversation_admin_dismissal_menu = "🗑 Remove Manager" + +# Conversation: administrator removal confirmation +conversation_confirm_admin_dismissal = "🗑 User was successfully removed from the Managers!" # Conversation: switching to user mode conversation_switch_to_user_mode = " You are switching to 👤 Customer mode.\n" \ @@ -296,6 +301,9 @@ "It might take a while... Please be patient!\n" \ "I won't be able to answer you while I'm downloading." +downloading_image_failed = "Something went wrong with the image upload handling" \ + "Please, try again" + # Edit product: current value edit_current_value = "The current value is:\n" \ "
{value}\n" \
diff --git a/strings/it_IT.py b/strings/it_IT.py
index 5499ede..c905953 100644
--- a/strings/it_IT.py
+++ b/strings/it_IT.py
@@ -115,8 +115,13 @@
conversation_open_help_menu = "Che tipo di assistenza desideri ricevere?"
# Conversation: confirm promotion to admin
-conversation_confirm_admin_promotion = "Sei sicuro di voler promuovere questo utente a 💼 Gestore?\n" \
- "E' un'azione irreversibile!"
+conversation_confirm_admin_promotion = "Sei sicuro di voler promuovere questo utente a 💼 Gestore?\n"
+
+# Conversation: remove administrator
+conversation_admin_dismissal_menu = "🗑 Elimina Gestore"
+
+# Conversation: administrator removal confirmation
+conversation_confirm_admin_dismissal = "🗑 Gestore eliminato con successo!"
# Conversation: switching to user mode
conversation_switch_to_user_mode = "Stai passando alla modalità 👤 Cliente.\n" \
@@ -297,6 +302,10 @@
"Potrei metterci un po'... Abbi pazienza!\n" \
"Non sarò in grado di risponderti durante il download."
+# There was an error reading the image the user sent
+downloading_image_failed = "Si è verificato un errore durante il download dell'immagine...\n" \
+ "Riprova, per favore!"
+
# Edit product: current value
edit_current_value = "Il valore attuale è:\n" \
"{value}\n" \
diff --git a/strings/ru_RU.py b/strings/ru_RU.py
index e093572..440f3ca 100644
--- a/strings/ru_RU.py
+++ b/strings/ru_RU.py
@@ -111,8 +111,13 @@
conversation_open_help_menu = "Чем могу Вам помочь?"
# Conversation: confirm promotion to admin
-conversation_confirm_admin_promotion = "Вы уверены, что хотите повысить этого пользователя до 💼 Менеджера?\n" \
- "Это действие невозможно отменить!"
+conversation_confirm_admin_promotion = "Вы уверены, что хотите повысить этого пользователя до 💼 Менеджера?\n"
+
+# Conversation: remove administrator
+conversation_admin_dismissal_menu = "🗑 Удалить менеджера"
+
+# Conversation: administrator removal confirmation
+conversation_confirm_admin_dismissal = "🗑 Менеджер удален"
# Conversation: switching to user mode
conversation_switch_to_user_mode = " Вы перешли в режим 👤 Покупателя.\n" \
@@ -265,7 +270,7 @@
# Edit credit: notes?
ask_transaction_notes = " Добавьте сообщение к транзакции.\n" \
" Сообщение будет доступно 👤 Покупателю после пополнения/списания средств" \
- " и 💼 Администратору в логах транзакций."
+ " и 💼 Менеджеру в логах транзакций."
# Edit credit: amount?
ask_credit = "Вы хотите изменить баланс Покупателя?\n" \
@@ -294,6 +299,9 @@
"Это может занять некоторое время...!\n" \
"Я не смогу отвечать, пока идет загрузка."
+downloading_image_failed = "Ошибка при загрузке изображения" \
+ "Попробуйте еще раз..."
+
# Edit product: current value
edit_current_value = "Текущее значение:\n" \
"{value}\n" \
diff --git a/strings/uk_UA.py b/strings/uk_UA.py
index a9afe08..768d87b 100644
--- a/strings/uk_UA.py
+++ b/strings/uk_UA.py
@@ -111,8 +111,13 @@
conversation_open_help_menu = "Як можемо Вам допомогти?"
# Conversation: confirm promotion to admin
-conversation_confirm_admin_promotion = "Ви впевнені, що хочете підвищити цього користувача до 💼 Менеджера?\n" \
- "Цю дію неможливо відмінити!"
+conversation_confirm_admin_promotion = "Ви впевнені, що хочете підвищити цього користувача до 💼 Менеджера?\n"
+
+# Conversation: remove administrator
+conversation_admin_dismissal_menu = "🗑 Видалити менеджера"
+
+# Conversation: administrator removal confirmation
+conversation_confirm_admin_dismissal = "🗑 Администратора видалено"
# Conversation: switching to user mode
conversation_switch_to_user_mode = " Ви перейшли в режим 👤 Замовника.\n" \
@@ -265,7 +270,7 @@
# Edit credit: notes?
ask_transaction_notes = " Додайте повідомлення до транзакції.\n" \
"👤 Повідомлення буде доступне замовнику після поповнення/списання" \
- " і 💼 Адміністратору в логах транзакцій."
+ " і 💼 Менеджеру в логах транзакцій."
# Edit credit: amount?
ask_credit = "Як ви хочете змінити баланс замовника?\n" \
@@ -294,6 +299,9 @@
"Може зайняти деякий час... Майте терпіння!\n" \
"Я не зможу відповідати, поки йде завантаження."
+downloading_image_failed = "Помилка при завантаженні зображення" \
+ "Спробуйте ще раз..."
+
# Edit product: current value
edit_current_value = "Поточне значення:\n" \
"{value}\n" \
diff --git a/utils.py b/utils.py
index 296cbeb..ae8a32f 100644
--- a/utils.py
+++ b/utils.py
@@ -1,11 +1,13 @@
+import importlib
+import os
+import sys
+import time
+import typing
+
import telegram
import telegram.error
-import time
+
from configloader import config
-import typing
-import os
-import sys
-import importlib
language = config["Config"]["language"]
try:
diff --git a/worker.py b/worker.py
index e9126c4..c70c896 100644
--- a/worker.py
+++ b/worker.py
@@ -1,19 +1,23 @@
+import importlib
+import os
+import queue as queuem
+import re
+import sys
import threading
-from typing import *
+import traceback
import uuid
-import datetime
+from datetime import datetime
+from html import escape
+from operator import attrgetter
+from typing import Dict, List, Optional, Union
+
+import requests
import telegram
+from sqlalchemy import or_
+
import configloader
-import sys
-import queue as queuem
import database as db
-import re
import utils
-import os
-import traceback
-from html import escape
-import requests
-import importlib
language = configloader.config["Config"]["language"]
strings = importlib.import_module("strings." + language)
@@ -91,6 +95,10 @@ def run(self):
self.session.add(self.admin)
# Commit the transaction
self.session.commit()
+ else:
+ # Update users last seen date
+ self.user.last_seen = datetime.now()
+ self.session.commit()
# Capture exceptions that occour during the conversation
try:
# If the user is not an admin, send him to the user menu
@@ -200,7 +208,7 @@ def __wait_for_regex(self, regex: str, cancellable: bool = False) -> Union[str,
if match is None:
continue
# Return the first capture group
- return match.group(1)
+ return match.group(1) or match.group(2)
def __wait_for_precheckoutquery(self,
cancellable: bool = False) -> Union[telegram.PreCheckoutQuery, CancelSignal]:
@@ -258,8 +266,21 @@ def __wait_for_photo(self, cancellable: bool = False) -> Union[List[telegram.Pho
# Ensure the message contains a photo
if update.message.photo is None:
continue
- # Return the photo array
- return update.message.photo
+ # If photo message has been sent
+ if len(update.message.photo) > 0:
+ # Find object with maximum width
+ photo = max(update.message.photo, key=attrgetter('width'))
+ break
+ else:
+ self.bot.send_message(self.chat.id, strings.downloading_image_failed)
+ continue
+
+ # If a photo has been sent...
+ # 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")
+ # Return file object associated with the photo
+ return self.bot.get_file(photo.file_id)
def __wait_for_inlinekeyboard_callback(self, cancellable: bool = False) \
-> Union[telegram.CallbackQuery, CancelSignal]:
@@ -286,8 +307,11 @@ def __wait_for_inlinekeyboard_callback(self, cancellable: bool = False) \
def __user_select(self) -> Union[db.User, CancelSignal]:
"""Select an user from the ones in the database."""
- # Find all the users in the database
- users = self.session.query(db.User).order_by(db.User.user_id).all()
+ # Find first five users in the database
+ users = self.session.query(db.User) \
+ .order_by(db.User.last_seen.desc()) \
+ .limit(5) \
+ .all()
# Create a list containing all the keyboard button strings
keyboard_buttons = [[strings.menu_cancel]]
# Add to the list all the users
@@ -300,12 +324,14 @@ def __user_select(self) -> Union[db.User, CancelSignal]:
# 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)
+ reply = self.__wait_for_regex("user_([0-9]+)|\@(.*)", cancellable=True)
# Propagate CancelSignals
if isinstance(reply, CancelSignal):
return reply
# Find the user in the database
- user = self.session.query(db.User).filter_by(user_id=int(reply)).one_or_none()
+ user = self.session.query(db.User) \
+ .filter(or_(db.User.user_id==reply, db.User.username==reply)) \
+ .one_or_none()
# Ensure the user exists
if not user:
self.bot.send_message(self.chat.id, strings.error_user_does_not_exist)
@@ -492,7 +518,7 @@ def __order_menu(self):
notes = self.__wait_for_regex(r"(.*)", cancellable=True)
# Create a new Order
order = db.Order(user=self.user,
- creation_date=datetime.datetime.now(),
+ creation_date=datetime.now(),
notes=notes if not isinstance(notes, CancelSignal) else "")
# Add the record to the session and get an ID
self.session.add(order)
@@ -805,7 +831,7 @@ def __products_menu(self):
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, cancellable = True)
+ selection = self.__wait_for_specific_message(product_names, cancellable=True)
# If the user has selected the Cancel option...
if isinstance(selection, CancelSignal):
# Exit the menu
@@ -875,10 +901,7 @@ def __edit_product_menu(self, product: Optional[db.Product] = None):
price = None
else:
price = utils.Price(price)
- # Ask for the product image
- self.bot.send_message(self.chat.id, strings.ask_product_image, reply_markup=cancel)
- # Wait for an answer
- photo_list = self.__wait_for_photo(cancellable=True)
+
# If a new product is being added...
if not product:
# Create the db record for the product
@@ -895,20 +918,13 @@ def __edit_product_menu(self, product: Optional[db.Product] = None):
product.name = name if not isinstance(name, CancelSignal) else product.name
product.description = description if not isinstance(description, CancelSignal) else product.description
product.price = int(price) if not isinstance(price, CancelSignal) else product.price
- # If a photo has been sent...
- if isinstance(photo_list, list):
- # 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)
+ # Ask for the product image
+ self.bot.send_message(self.chat.id, strings.ask_product_image, reply_markup=cancel)
+ # Wait for an answer
+ photo = self.__wait_for_photo(cancellable=True)
+ # Set the image for that product
+ if not isinstance(photo, CancelSignal):
+ product.set_image(photo)
# Commit the session changes
self.session.commit()
# Notify the user
@@ -927,7 +943,7 @@ def __delete_product_menu(self):
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, cancellable = True)
+ selection = self.__wait_for_specific_message(product_names, cancellable=True)
if isinstance(selection, CancelSignal):
# Exit the menu
return
@@ -988,7 +1004,7 @@ def __orders_menu(self):
# If the user pressed the complete order button, complete the order
if update.data == "order_complete":
# Mark the order as complete
- order.delivery_date = datetime.datetime.now()
+ order.delivery_date = datetime.now()
# Commit the transaction
self.session.commit()
# Update order message
@@ -1011,7 +1027,7 @@ def __orders_menu(self):
self.bot.delete_message(self.chat.id, reason_msg.message_id)
continue
# Mark the order as refunded
- order.refund_date = datetime.datetime.now()
+ order.refund_date = datetime.now()
# Save the refund reason
order.refund_reason = reply
# Refund the credit, reverting the old transaction
@@ -1199,66 +1215,83 @@ def __transactions_file(self):
# Delete the created file
os.remove(f"transactions_{self.chat.id}.csv")
- def __add_admin(self):
+ def __select_or_create_admin(self):
"""Add an administrator to the bot."""
# Let the admin select an administrator to promote
user = self.__user_select()
# Allow the cancellation of the operation
if isinstance(user, CancelSignal):
- return
+ return None, None
# Check if the user is already an administrator
admin = self.session.query(db.Admin).filter_by(user_id=user.user_id).one_or_none()
+ # Return if user is not an admin
if admin is None:
# Create the keyboard to be sent
keyboard = telegram.ReplyKeyboardMarkup([[strings.emoji_yes, strings.emoji_no]], one_time_keyboard=True)
- # Ask for confirmation
+ # Ask for confirmation on the admin deletion
self.bot.send_message(self.chat.id, strings.conversation_confirm_admin_promotion, reply_markup=keyboard)
# Wait for an answer
selection = self.__wait_for_specific_message([strings.emoji_yes, strings.emoji_no])
# Proceed only if the answer is yes
if selection == strings.emoji_no:
- return
+ return None, None
# Create a new admin
admin = db.Admin(user=user,
- edit_products=False,
- receive_orders=False,
- create_transactions=False,
- is_owner=False,
- display_on_help=False)
+ edit_products=False,
+ receive_orders=False,
+ create_transactions=False,
+ is_owner=False,
+ display_on_help=False)
self.session.add(admin)
+ self.session.commit()
+
+ return user
+
+ def __add_admin(self):
+ """Add an administrator to the bot."""
+ user = self.__select_or_create_admin()
+
+ if (user is None or user.admin is None):
+ return
+
# 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, strings.admin_properties.format(name=str(user)))
# Start accepting edits
while True:
# Create the inline keyboard with the admin status
inline_keyboard = telegram.InlineKeyboardMarkup([
[telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.edit_products)} {strings.prop_edit_products}",
- callback_data="toggle_edit_products")],
+ callback_data="toggle_edit_products")],
[telegram.InlineKeyboardButton(f"{utils.boolmoji(admin.receive_orders)} {strings.prop_receive_orders}",
- callback_data="toggle_receive_orders")],
+ callback_data="toggle_receive_orders")],
[telegram.InlineKeyboardButton(
f"{utils.boolmoji(admin.create_transactions)} {strings.prop_create_transactions}",
callback_data="toggle_create_transactions")],
[telegram.InlineKeyboardButton(
f"{utils.boolmoji(admin.display_on_help)} {strings.prop_display_on_help}",
callback_data="toggle_display_on_help")],
+ [telegram.InlineKeyboardButton(strings.conversation_admin_dismissal_menu, callback_data="cmd_remove")],
[telegram.InlineKeyboardButton(strings.menu_done, callback_data="cmd_done")]
])
# Update the inline keyboard
self.bot.edit_message_reply_markup(message_id=message.message_id,
- chat_id=self.chat.id,
- reply_markup=inline_keyboard)
+ chat_id=self.chat.id,
+ reply_markup=inline_keyboard)
# Wait for an user answer
callback = self.__wait_for_inlinekeyboard_callback()
# Toggle the correct property
if callback.data == "toggle_edit_products":
- admin.edit_products = not admin.edit_products1
+ admin.edit_products = not admin.edit_products
elif callback.data == "toggle_receive_orders":
admin.receive_orders = not admin.receive_orders
elif callback.data == "toggle_create_transactions":
admin.create_transactions = not admin.create_transactions
elif callback.data == "toggle_display_on_help":
admin.display_on_help = not admin.display_on_help
+ elif callback.data == "cmd_remove":
+ self.session.delete(user.admin)
+ message = self.bot.send_message(self.chat.id, strings.conversation_confirm_admin_dismissal)
+ break
elif callback.data == "cmd_done":
break
self.session.commit()