From f4446ed6fb1ecc6c1ad92ee7518821456502ce6a Mon Sep 17 00:00:00 2001 From: Joan Date: Fri, 10 Mar 2023 23:52:25 +0100 Subject: [PATCH] Modified categories. Remove buttons when used. Added timeout to conversation. Check if user is expired. Added telegram name. Fixed bug with lowercase. --- wallamanta/constants.py | 48 +++++++++++++-------------- wallamanta/helpers.py | 46 +++++++++++++++++--------- wallamanta/requirements.txt | 1 + wallamanta/walladb.py | 28 ++++++++++++---- wallamanta/wallamanta.py | 65 +++++++++++++++++-------------------- wallamanta/worker.py | 8 +++-- 6 files changed, 110 insertions(+), 86 deletions(-) diff --git a/wallamanta/constants.py b/wallamanta/constants.py index 35a7144..7773509 100644 --- a/wallamanta/constants.py +++ b/wallamanta/constants.py @@ -10,28 +10,26 @@ LATITUDE = os.getenv("LATITUDE") LONGITUDE = os.getenv("LONGITUDE") SLEEP_TIME = int(os.getenv("SLEEP_TIME")) -CATEGORIES = {'coches': 100, - 'motos': 14000, - 'motor y accesorios': 12800, - 'moda y accesorios': 12465, - 'inmobiliaria': 200, - 'tv audio y foto': 12545, - 'móviles y telefonía': 16000, - 'informática y electrónica': 15000, - 'informática': 15000, - 'electrónica': 15000, - 'deporte y ocio': 12579, - 'bicicletas': 17000, - 'consolas y videojuegos': 12900, - 'consolas': 12900, - 'videojuegos': 12900, - 'hogar y jardín': 12467, - 'electrodomésticos': 13100, - 'cine libros y música': 12463, - 'niños y bebés': 12461, - 'coleccionismo': 18000, - 'construcción y reformas': 19000, - 'industria y agricultura': 20000, - 'empleo': 21000, - 'servicios': 13200, - 'otros': 12485} \ No newline at end of file +CATEGORIES = {'🚗 Coches 🚗': 100, + '🏍️ Motos 🏍️': 14000, + '🚙 Motor 🚙': 12800, + '👗 Moda 👗': 12465, + '🏘️ Inmobiliaria 🏘️': 200, + '📺 TV, audio y foto 📺': 12545, + '📱 Móviles 📱': 16000, + '🖥 Informática 🖥️': 15000, + '🏅 Deportes y ocio 🏅': 12579, + '🚴 Bicicletas 🚴': 17000, + '🎮 Consolas y videojuegos 🎮': 12900, + '🏡 Hogar y jardín 🏡': 12467, + 'Electrodomésticos': 13100, + '🎥 Cine, libros y música 🎥': 12463, + '🧒 Niños y bebés 🧒': 12461, + 'Coleccionismo': 18000, + 'Construcción y reformas': 19000, + 'Industria y agricultura': 20000, + 'Empleo': 21000, + 'Servicios': 13200, + 'Otros': 12485, + 'Todas': 0} + \ No newline at end of file diff --git a/wallamanta/helpers.py b/wallamanta/helpers.py index d935bb2..c1ab375 100644 --- a/wallamanta/helpers.py +++ b/wallamanta/helpers.py @@ -35,10 +35,18 @@ def get_telegram_user_id(update): #return update.message.chat_id return update.effective_chat.id +def get_telegram_user_name(update): + return update.message.from_user + def get_date_ahead(add_days): date_ahead = datetime.today() + timedelta(days=add_days) return date_ahead.strftime("%d/%m/%Y") +def is_date_expired(until): + until_date = datetime.strptime(until, "%d/%m/%Y") + difference = until_date - datetime.today() + return difference.days < 0 + def random_wait(): time.sleep(random.random()) @@ -114,32 +122,40 @@ def get_category_id(category): ret = constants.CATEGORIES.get(category, '') return ret +def get_category_name(category): + category_name = '' + for i_category in constants.CATEGORIES: + if constants.CATEGORIES[i_category] == category: + category_name = i_category + break + return category_name + def create_category_keyboard(): keyboard = [ [ - InlineKeyboardButton("🖥 Informática", callback_data='15000'), - InlineKeyboardButton("Motor", callback_data='12800'), - InlineKeyboardButton("Coches", callback_data='100'), + InlineKeyboardButton("🖥 Informática 🖥️ ", callback_data='15000'), + InlineKeyboardButton("🚙 Motor 🚙", callback_data='12800'), + InlineKeyboardButton("🚗 Coches 🚗", callback_data='100'), ], [ - InlineKeyboardButton("Motos", callback_data='14000'), - InlineKeyboardButton("Moda", callback_data='12465'), - InlineKeyboardButton("Inmobiliaria", callback_data='200'), + InlineKeyboardButton("🏍️ Motos 🏍️", callback_data='14000'), + InlineKeyboardButton("👗 Moda 👗", callback_data='12465'), + InlineKeyboardButton("🏘️ Inmobiliaria 🏘️", callback_data='200'), ], [ - InlineKeyboardButton("TV, audio y foto", callback_data='12545'), - InlineKeyboardButton("Móviles", callback_data='16000'), - InlineKeyboardButton("Deportes y ocio", callback_data='12579'), + InlineKeyboardButton("📺 TV, audio y foto 📺", callback_data='12545'), + InlineKeyboardButton("📱 Móviles 📱", callback_data='16000'), + InlineKeyboardButton("🏅 Deportes y ocio 🏅", callback_data='12579'), ], [ - InlineKeyboardButton("Bicicletas", callback_data='17000'), - InlineKeyboardButton("Consolas y videojuegos", callback_data='12900'), - InlineKeyboardButton("Hogar y jardín", callback_data='12467'), + InlineKeyboardButton("🚴 Bicicletas 🚴", callback_data='17000'), + InlineKeyboardButton("🎮 Consolas y videojuegos 🎮", callback_data='12900'), + InlineKeyboardButton("🏡 Hogar y jardín 🏡", callback_data='12467'), ], [ InlineKeyboardButton("Electrodomésticos", callback_data='13100'), - InlineKeyboardButton("Cine, libros y música", callback_data='12463'), - InlineKeyboardButton("Niños y bebés", callback_data='12461'), + InlineKeyboardButton("🎥 Cine, libros y música 🎥", callback_data='12463'), + InlineKeyboardButton("🧒 Niños y bebés 🧒", callback_data='12461'), ], [ InlineKeyboardButton("Coleccionismo", callback_data='18000'), @@ -164,7 +180,7 @@ def create_continue_keyboard(): InlineKeyboardButton("Descartar", callback_data='remove') ],[ InlineKeyboardButton("Exclusión de título", callback_data='title_exclude'), - InlineKeyboardButton("Exclusión de descripción", callback_data='description_exclude') + InlineKeyboardButton("Exclusión de descripción", callback_data='title_description_exclude') ],[ InlineKeyboardButton("Coordenadas", callback_data='coords'), InlineKeyboardButton("Distancia", callback_data='distance') diff --git a/wallamanta/requirements.txt b/wallamanta/requirements.txt index 47cd6d5..200a22e 100644 --- a/wallamanta/requirements.txt +++ b/wallamanta/requirements.txt @@ -1,4 +1,5 @@ python-telegram-bot==20.1 +python-telegram-bot[job-queue]==20.1 requests==2.28.1 prettytable==3.6.0 Pillow==9.4.0 \ No newline at end of file diff --git a/wallamanta/walladb.py b/wallamanta/walladb.py index 0561fb3..ac3688e 100644 --- a/wallamanta/walladb.py +++ b/wallamanta/walladb.py @@ -1,7 +1,7 @@ import sqlite3 -import os import logging import constants +import helpers # Enable logging logging.basicConfig( @@ -19,7 +19,7 @@ def dict_factory(cursor, row): def setup_db(): con = sqlite3.connect(constants.DB) cur = con.cursor() - cur.execute("CREATE TABLE IF NOT EXISTS users(telegram_user_id, active, type, until)") + cur.execute("CREATE TABLE IF NOT EXISTS users(telegram_user_id, active, type, until, telegram_name)") cur.execute("CREATE TABLE IF NOT EXISTS products(product_name, distance, \ latitude, longitude, condition, min_price, max_price, category, \ title_exclude, title_description_exclude, telegram_user_id)") @@ -33,6 +33,18 @@ def is_user_valid(telegram_user_id): con.close() return ret +def is_user_expired(telegram_user_id): + con = sqlite3.connect(constants.DB) + cur = con.cursor() + res = cur.execute(f"SELECT until FROM users WHERE telegram_user_id={telegram_user_id}") + q_res = res.fetchone() + ret = True + if q_res != None: + if not helpers.is_date_expired(q_res[0]): + ret = False + con.close() + return ret + def is_user_premium(telegram_user_id): con = sqlite3.connect(constants.DB) cur = con.cursor() @@ -49,13 +61,14 @@ def is_user_testing(telegram_user_id): con.close() return ret -def add_premium_user(telegram_user_id, until): +def add_premium_user(telegram_user_id, telegram_name, until): found = False con = sqlite3.connect(constants.DB) cur = con.cursor() res = cur.execute(f"SELECT * FROM users WHERE telegram_user_id={telegram_user_id}") if res.fetchone() is None: - cur.execute(f"INSERT INTO users VALUES ({telegram_user_id}, True, 'premium', '{until}')") + params = (telegram_user_id, True, 'premium', until, telegram_name.first_name) + cur.execute("INSERT INTO users VALUES (?, ?, ?, ?, ?)", params) con.commit() else: cur.execute(f"UPDATE users SET active = True, type = 'premium', until = '{until}' WHERE telegram_user_id={telegram_user_id}") @@ -65,13 +78,14 @@ def add_premium_user(telegram_user_id, until): logging.info(f"Added premium user {telegram_user_id} until {until}") return found -def add_test_user(telegram_user_id, until): +def add_test_user(telegram_user_id, telegram_name, until): found = False con = sqlite3.connect(constants.DB) cur = con.cursor() res = cur.execute(f"SELECT * FROM users WHERE telegram_user_id={telegram_user_id}") if res.fetchone() is None: - cur.execute(f"INSERT INTO users VALUES ({telegram_user_id}, True, 'testing', '{until}')") + params = (telegram_user_id, True, 'testing', until, telegram_name.first_name) + cur.execute("INSERT INTO users VALUES (?, ?, ?, ?, ?)", params) con.commit() else: found = True @@ -114,7 +128,7 @@ def get_user_until(telegram_user_id): return ret[0] def get_product(product): - product_name = product.get('product_name') + product_name = product.get('product_name').lower() telegram_user_id = product.get('telegram_user_id') con = sqlite3.connect(constants.DB) con.row_factory = dict_factory diff --git a/wallamanta/wallamanta.py b/wallamanta/wallamanta.py index e02bf7b..70f5302 100644 --- a/wallamanta/wallamanta.py +++ b/wallamanta/wallamanta.py @@ -31,28 +31,11 @@ logging.basicConfig( logger = logging.getLogger(__name__) -def parse_json_file(): - f = open("/app/data/products.json") - return json.load(f) - -def save_json_file(products): - with open('/app/data/products.json', 'w') as outfile: - json.dump(products, outfile, indent=2) - async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if walladb.is_user_valid(helpers.get_telegram_user_id(update)): - message = """Añade un producto con `/add producto;precio_mínimo;precio_máximo,categoría,excluir_título(opcional, separado por comas); -excluir_descripción_y_título(opciona, separado por comas);latitud(opcional);longitud(opcional),distancia(opcional)`\n -Ejemplo: `/add placa base itx;0;150`\n -Ejemplo 2: `/add cpu;10;30;;;intel,core 2 duo,celeron;;;100`\n -Ejemplo 3: `/add tiny;0;100;informática`\n -Los campos opcionales que se dejen vacíos tomarán el valor configurado en el archivo `.env`\n -Lista los productos con `/list` o obtén la información de uno en concreto con `/list nombre del producto`\n -Borra un producto con `/remove nombre del producto`\n -`/status` muestra tu tipo de membresía y fecha de caducidad\n -`/categories` muestra las categorías disponibles""" + message = "Utiliza el menú de la conversación para añadir un producto y sigue los pasos indicados" else: - message = """Activa tu periodo de prueba de 7 días con `/test` o contacta con @jocarduck para más información.""" + message = "Activa tu periodo de prueba de 7 días con `/test` o contacta con @jocarduck para más información." await update.message.reply_markdown_v2(helpers.telegram_escape_characters(message)) async def main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -68,6 +51,7 @@ async def main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: return ACTION async def menu_click_handler(update: Update, context: CallbackContext): + await update.callback_query.edit_message_reply_markup(None) telegram_user_id = helpers.get_telegram_user_id(update) if walladb.is_user_valid(telegram_user_id): context.user_data['telegram_user_id'] = telegram_user_id @@ -99,11 +83,10 @@ async def menu_click_handler(update: Update, context: CallbackContext): return REMOVE_PRODUCT if query.data == 'list': await send_list(update, context) - #await context.bot.send_message(chat_id=update.effective_chat.id, text='Send your name', reply_markup=ForceReply()) return ConversationHandler.END else: await context.bot.send_message(chat_id=update.effective_chat.id, - text='Activa tu periodo de prueba de 7 días con `/test` o contacta con @jocarduck para más información.', parse_mode=ParseMode.MARKDOWN_V2) + text=helpers.telegram_escape_characters('Activa tu periodo de prueba de 7 días con `/test` o contacta con @jocarduck para más información.'), parse_mode=ParseMode.MARKDOWN_V2) return ConversationHandler.END async def add_product_name(update: Update, context: CallbackContext): @@ -162,6 +145,7 @@ async def add_product_max_price(update: Update, context: CallbackContext): return ADD_PRODUCT_CATEGORY async def add_product_category(update: Update, context: CallbackContext): + await update.callback_query.edit_message_reply_markup(None) query = update.callback_query if query.data == 'category': markup = InlineKeyboardMarkup(helpers.create_category_keyboard()) @@ -177,6 +161,8 @@ async def add_product_category(update: Update, context: CallbackContext): return ConversationHandler.END async def continue_or_finish(update: Update, context: CallbackContext): + if update.callback_query != None: + await update.callback_query.edit_message_reply_markup(None) markup = InlineKeyboardMarkup(helpers.create_continue_keyboard()) last_step = context.user_data.get('last_step', '') query = update.callback_query @@ -195,6 +181,8 @@ async def continue_or_finish(update: Update, context: CallbackContext): if last_step == 'category': category = int(query.data) + await context.bot.send_message(chat_id=update.effective_chat.id, + text=f"Categoría escogida: {helpers.get_category_name(category)}") context.user_data['category'] = category context.user_data['last_step'] = '' @@ -207,13 +195,13 @@ async def continue_or_finish(update: Update, context: CallbackContext): context.user_data['title_exclude'] = update.message.text context.user_data['last_step'] = '' - if qd == 'description_exclude': + if qd == 'title_description_exclude': await context.bot.send_message(chat_id=update.effective_chat.id, text='Exclusión de descripción', reply_markup=ForceReply()) - context.user_data['last_step'] = 'description_exclude' + context.user_data['last_step'] = 'title_description_exclude' return CONTINUE_OR_FINISH - if last_step == 'description_exclude': - context.user_data['description_exclude'] = update.message.text + if last_step == 'title_description_exclude': + context.user_data['title_description_exclude'] = update.message.text context.user_data['last_step'] = '' if qd == 'coords': @@ -255,6 +243,7 @@ async def continue_or_finish(update: Update, context: CallbackContext): return CONTINUE_OR_FINISH async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + logging.info('Conversation cancelled') context.user_data.clear() await context.bot.send_message(chat_id=update.effective_chat.id, text="Cancelado.") return ConversationHandler.END @@ -306,7 +295,7 @@ async def add_premium_user_command(update: Update, context: ContextTypes.DEFAULT telegram_user_id = update.message.text.split('/add_premium_user ')[1].split(' ')[0] days = update.message.text.split('/add_premium_user ')[1].split(' ')[1] until = helpers.get_date_ahead(int(days)) - if not walladb.add_premium_user(telegram_user_id, until): + if not walladb.add_premium_user(telegram_user_id, '', until): products = walladb.get_products_from_user(telegram_user_id) for product in products: @@ -333,9 +322,10 @@ async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> async def test_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: telegram_user_id = helpers.get_telegram_user_id(update) + telegram_user_name = helpers.get_telegram_user_name(update) if not walladb.is_user_valid(telegram_user_id): until = helpers.get_date_ahead(7) - walladb.add_test_user(telegram_user_id, until) + walladb.add_test_user(telegram_user_id, telegram_user_name, until) message = f"Periodo de prueba activado hasta el {until}." else: message = "Ya has utilizado el periodo de prueba." @@ -345,7 +335,7 @@ async def test_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No message = "Ya eres premium. No puedes volver al periodo de prueba." await update.message.reply_markdown_v2(helpers.telegram_escape_characters(message)) -async def add_to_db_and_send(update, context): +async def add_to_db_and_send(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: logging.info(f"Adding product with context: {context.user_data}") walladb.add_product(context.user_data) p = threading.Thread(target=Worker.run, args=(walladb.get_product(context.user_data), )) @@ -355,6 +345,12 @@ async def add_to_db_and_send(update, context): def error(update, context): logging.error(f'Update ---{update}--- caused error ---{context.error}---') +async def conv_timeout(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + await context.bot.send_message(chat_id=update.effective_chat.id, text="Se ha superado el tiempo de espera, vuelve a usar el menú si quieres añadir otro producto.") + +async def conv_finish(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + await context.bot.send_message(chat_id=update.effective_chat.id, text="Vuelve a usar el menú si quieres añadir otro producto.") + def main()->None: walladb.setup_db() products = walladb.get_all_products() @@ -366,7 +362,7 @@ def main()->None: """Start the bot.""" # Create the Application and pass it your bot's token. - application = Application.builder().token(constants.TELEGRAM_TOKEN).build() + application = Application.builder().get_updates_http_version('1.1').http_version('1.1').token(constants.TELEGRAM_TOKEN).build() # on different commands - answer in Telegram application.add_handler(CommandHandler("help", help_command)) @@ -377,6 +373,7 @@ def main()->None: application.add_handler(CommandHandler("test", test_command)) conv_handler = ConversationHandler( + conversation_timeout=60, entry_points=[CommandHandler("menu", main_menu)], states={ ACTION: [CallbackQueryHandler(menu_click_handler)], @@ -388,20 +385,16 @@ def main()->None: CONTINUE_OR_FINISH: [CallbackQueryHandler(continue_or_finish), MessageHandler(filters.TEXT, continue_or_finish)], REMOVE_PRODUCT: [MessageHandler(filters.TEXT, remove_product)], + ConversationHandler.TIMEOUT: [CallbackQueryHandler(conv_timeout), + MessageHandler(filters.TEXT, conv_timeout)], + ConversationHandler.END: [MessageHandler(filters.TEXT, conv_finish)], }, fallbacks=[CommandHandler("cancel", cancel)], ) - # Add ConversationHandler to application that will be used for handling updates application.add_handler(conv_handler) - - #application.bot.set_chat_menu_button('Menú') - application.add_error_handler(error) - # on non command i.e message - echo the message on Telegram - #application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) - # Run the bot until the user presses Ctrl-C application.run_polling() if __name__ == "__main__": diff --git a/wallamanta/worker.py b/wallamanta/worker.py index 26c7329..af88c3d 100644 --- a/wallamanta/worker.py +++ b/wallamanta/worker.py @@ -17,9 +17,11 @@ class Worker: def is_valid_request(self, product): is_valid = False if walladb.get_product(product): - if walladb.is_user_valid(product['telegram_user_id']): - if walladb.is_user_premium(product['telegram_user_id']) or walladb.is_user_testing(product['telegram_user_id']): - is_valid = True + if not walladb.is_user_expired(product['telegram_user_id']): + if walladb.is_user_valid(product['telegram_user_id']): + if walladb.is_user_premium(product['telegram_user_id']) or \ + walladb.is_user_testing(product['telegram_user_id']): + is_valid = True return is_valid def request(self, product_name, n_articles, latitude=constants.LATITUDE, longitude=constants.LONGITUDE, distance='0', condition='all', min_price=0, max_price=10000000, category=""):