import threading import logging import helpers import walladb import constants import account_checker import time import search_manager from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ForceReply from telegram.constants import ParseMode from telegram.ext import ( ApplicationBuilder, CallbackQueryHandler, CallbackContext, CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters ) SEARCH_THREADS_LIST = [] ACTION, ADD_PRODUCT_NAME, ADD_PRODUCT_MIN_PRICE, ADD_PRODUCT_MAX_PRICE, \ ADD_PRODUCT_CATEGORY, ADD_PRODUCT_TITLE_EXCLUDE, ADD_PRODUCT_DESCRIPTION_EXCLUDE, \ ADD_PRODUCT_COORDS, ADD_PRODUCT_DISTANCE, REMOVE_PRODUCT, LIST, FINISH, CONTINUE_OR_FINISH = range(13) MAX_PRODUCTS_TESTING = 5 MAX_PRODUCTS_PREMIUM = 5 MAX_PRODUCTS_HOMELABS = 5 # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) httpx_logger = logging.getLogger('httpx') httpx_logger.setLevel(logging.WARNING) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if walladb.is_user_active(helpers.get_telegram_user_id(update)): message = "Utiliza el menú de la conversación para añadir un producto y sigue los pasos indicados. Si tienes cualquier duda contacta con @jocarduck para más información." else: message = "No tienes permitido usar este bot." await update.message.reply_markdown_v2(helpers.telegram_escape_characters(message)) async def main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: context.user_data.clear() keyboard = [ [ InlineKeyboardButton("Añadir", callback_data='add'), InlineKeyboardButton("Listar", callback_data='list'), InlineKeyboardButton("Borrar", callback_data='remove')] ] markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text('Selecciona una acción a realizar', reply_markup=markup) 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_active(telegram_user_id): context.user_data['telegram_user_id'] = telegram_user_id query = update.callback_query number_of_products = walladb.count_user_products(telegram_user_id) if query.data == 'add': valid = False number_of_products = walladb.count_user_products(telegram_user_id) if walladb.is_user_testing(telegram_user_id): valid = True if number_of_products >= MAX_PRODUCTS_TESTING: message = f"Ya tienes {MAX_PRODUCTS_TESTING} productos en seguimiento. Borra algunos para añadir más." valid = False elif walladb.is_user_premium(telegram_user_id): valid = True if number_of_products >= MAX_PRODUCTS_PREMIUM: message = f"Ya tienes {MAX_PRODUCTS_PREMIUM} productos en seguimiento. Borra algunos para añadir más." valid = False elif walladb.is_user_homelabs(telegram_user_id): valid = True if number_of_products >= MAX_PRODUCTS_HOMELABS: message = f"Ya tienes {MAX_PRODUCTS_HOMELABS} productos en seguimiento. Borra algunos para añadir más." valid = False if valid: await context.bot.send_message(chat_id=update.effective_chat.id, text='Escribe el nombre del producto a buscar', reply_markup=ForceReply()) return ADD_PRODUCT_NAME else: await context.bot.send_message(chat_id=update.effective_chat.id, text=f'{message}') return ConversationHandler.END if query.data == 'remove': if number_of_products != 0: await send_list(update, context) return REMOVE_PRODUCT message = "No tienes ninguna búsqueda activa" await context.bot.send_message(chat_id=update.effective_chat.id, text=f'{message}') return ConversationHandler.END if query.data == 'list': if number_of_products != 0: await send_list(update, context) return LIST message = "No tienes ninguna búsqueda activa" await context.bot.send_message(chat_id=update.effective_chat.id, text=f'{message}') return ConversationHandler.END else: await context.bot.send_message(chat_id=update.effective_chat.id, text=helpers.telegram_escape_characters('No tienes permiso para usar este bot.'), parse_mode=ParseMode.MARKDOWN_V2) return ConversationHandler.END async def add_product_name(update: Update, context: CallbackContext): if context.user_data.get('product_name', '') == '': product_name = update.message.text if len(product_name) > 200: await context.bot.send_message(chat_id=update.effective_chat.id, text='El nombre del producto a buscar es muy largo, pon otro nombre', reply_markup=ForceReply()) return ADD_PRODUCT_NAME else: context.user_data['product_name'] = product_name if not walladb.get_product(context.user_data): await context.bot.send_message(chat_id=update.effective_chat.id, text='Escribe el precio mínimo', reply_markup=ForceReply()) return ADD_PRODUCT_MIN_PRICE else: await context.bot.send_message(chat_id=update.effective_chat.id, text=f"¡{walladb.get_product(context.user_data)['product_name']} ya está en seguimiento!") return ConversationHandler.END async def add_product_min_price(update: Update, context: CallbackContext): if context.user_data.get('min_price', '') == '': answer = update.message.text try: min_price = int(answer) except: await context.bot.send_message(chat_id=update.effective_chat.id, text='Pon un precio mínimo en números', reply_markup=ForceReply()) return ADD_PRODUCT_MIN_PRICE if min_price < 0 or min_price > 999999: await context.bot.send_message(chat_id=update.effective_chat.id, text='Pon un precio mínimo entre 0 y 999999', reply_markup=ForceReply()) return ADD_PRODUCT_MIN_PRICE context.user_data['min_price'] = min_price await context.bot.send_message(chat_id=update.effective_chat.id, text='Escribe el precio máximo', reply_markup=ForceReply()) return ADD_PRODUCT_MAX_PRICE async def add_product_max_price(update: Update, context: CallbackContext): if context.user_data.get('max_price', '') == '': answer = update.message.text try: max_price = int(answer) except: await context.bot.send_message(chat_id=update.effective_chat.id, text='Pon un precio máximo en números', reply_markup=ForceReply()) return ADD_PRODUCT_MAX_PRICE if max_price < 0 or max_price > 999999: await context.bot.send_message(chat_id=update.effective_chat.id, text='Pon un precio máximo entre 0 y 999999', reply_markup=ForceReply()) return ADD_PRODUCT_MAX_PRICE if max_price < context.user_data['min_price']: await context.bot.send_message(chat_id=update.effective_chat.id, text='Pon un precio máximo menor al precio mínimo', reply_markup=ForceReply()) return ADD_PRODUCT_MAX_PRICE context.user_data['max_price'] = max_price keyboard = [ [ InlineKeyboardButton("Añadir", callback_data='add'), InlineKeyboardButton("Descartar", callback_data='remove') ], [InlineKeyboardButton("Escoge una categoría", callback_data='category'),], ] markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(f'{context.user_data["product_name"]} de {context.user_data["min_price"]}€ a {context.user_data["max_price"]}€', reply_markup=markup) 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()) context.user_data['last_step'] = 'category' await context.bot.send_message(chat_id=update.effective_chat.id, text='Escoge la categoría', reply_markup=markup) return CONTINUE_OR_FINISH elif query.data == 'add': await add_to_db_and_send(update, context) return ConversationHandler.END elif query.data == 'remove': context.user_data.clear() await context.bot.send_message(chat_id=update.effective_chat.id, text=f'Borrado') 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 try: qd = query.data logging.info(f"Query Data: {qd}") except: qd = '' if qd == 'add': await add_to_db_and_send(update, context) return ConversationHandler.END if qd == 'remove': context.user_data.clear() await context.bot.send_message(chat_id=update.effective_chat.id, text=f'Borrado') return ConversationHandler.END if qd == 'add_subcategory': await context.bot.send_message(chat_id=update.effective_chat.id, text='Escoge la categoría', reply_markup=InlineKeyboardMarkup(helpers.create_categories_keyboard(context.user_data['category']))) context.user_data['last_step'] = 'choose_subcategory' return CONTINUE_OR_FINISH if qd == 'add_category': await context.bot.send_message(chat_id=update.effective_chat.id, text='Categoría', reply_markup=InlineKeyboardMarkup(helpers.create_category_keyboard())) context.user_data['last_step'] = 'category' return CONTINUE_OR_FINISH if last_step == "choose_subcategory": category = query.data if category != 'finish': await context.bot.send_message(chat_id=update.effective_chat.id, text='Escoge la subcategoría', reply_markup=InlineKeyboardMarkup(helpers.create_subcategory_keyboard(category))) context.user_data['last_step'] = 'subcategory' return CONTINUE_OR_FINISH else: context.user_data['last_step'] = '' if last_step == 'subcategory': subcategory = query.data if subcategory != 'finish': text = f"✅Subcategoría escogida: {helpers.get_subcategory_name(subcategory)}✅" if context.user_data.get('subcategory'): if subcategory not in context.user_data['subcategory'].split(','): context.user_data['subcategory'] = context.user_data['subcategory'] + ',' + subcategory else: text = f"❌La subcategoría {helpers.get_subcategory_name(subcategory)} ya estaba añadida❌" else: context.user_data['subcategory'] = subcategory await context.bot.send_message(chat_id=update.effective_chat.id, text=text) logging.info(f"{context.user_data['subcategory']}") context.user_data['last_step'] = '' if last_step == 'category': category = query.data if category != 'finish': text = f"✅Categoría escogida: {helpers.get_category_name(category)}✅" if context.user_data.get('category'): if category not in context.user_data['category'].split(','): context.user_data['category'] = context.user_data['category'] + ',' + category else: text = f"❌La categoría {helpers.get_category_name(category)} ya estaba añadida❌" else: context.user_data['category'] = category await context.bot.send_message(chat_id=update.effective_chat.id, text=text) logging.info(f"{context.user_data['category']}") context.user_data['last_step'] = '' if qd == 'title_exclude': await context.bot.send_message(chat_id=update.effective_chat.id, text='Exclusión de título', reply_markup=ForceReply()) context.user_data['last_step'] = 'title_exclude' return CONTINUE_OR_FINISH if last_step == 'title_exclude': context.user_data['title_exclude'] = update.message.text context.user_data['last_step'] = '' 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'] = 'title_description_exclude' return CONTINUE_OR_FINISH if last_step == 'title_description_exclude': context.user_data['title_description_exclude'] = update.message.text context.user_data['last_step'] = '' if qd == 'coords': await context.bot.send_message(chat_id=update.effective_chat.id, text='Coordenadas', reply_markup=ForceReply()) context.user_data['last_step'] = 'coords' return CONTINUE_OR_FINISH if last_step == 'coords': answer = update.message.text try: latitude = round(float(answer.split(',')[0]), 4) longitude = round(float(answer.split(',')[1]), 4) except: await context.bot.send_message(chat_id=update.effective_chat.id, text='Pon las coordenadas siguiendo el ejemplo: 41.34, 0.65', reply_markup=ForceReply()) return CONTINUE_OR_FINISH context.user_data['latitude'] = str(latitude) context.user_data['longitude'] = str(longitude) context.user_data['last_step'] = '' if qd == 'distance': await context.bot.send_message(chat_id=update.effective_chat.id, text='Distancia', reply_markup=ForceReply()) context.user_data['last_step'] = 'distance' return CONTINUE_OR_FINISH if last_step == 'distance': answer = update.message.text try: latitude = int(answer) except: await context.bot.send_message(chat_id=update.effective_chat.id, text='Pon la distancia en km, por ejemplo: 100', reply_markup=ForceReply()) return CONTINUE_OR_FINISH context.user_data['distance'] = answer context.user_data['last_step'] = '' if context.user_data['last_step'] == '': await context.bot.send_message(chat_id=update.effective_chat.id, text=f'Añadir?', reply_markup=markup) return CONTINUE_OR_FINISH async def add_subcategory(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: pass 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 async def remove_product(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: global SEARCH_THREADS_LIST await update.callback_query.edit_message_reply_markup(None) query = update.callback_query product_name = query.data telegram_user_id = helpers.get_telegram_user_id(update) message = f"¡{product_name} no está en seguimiento!" if walladb.remove_product({'product_name' : product_name, \ 'telegram_user_id' : telegram_user_id}): message = f"¡{product_name} borrado de la lista de seguimiento!" await context.bot.send_message(chat_id=update.effective_chat.id, text=message) return ConversationHandler.END async def product_details(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await update.callback_query.edit_message_reply_markup(None) query = update.callback_query product_name = query.data telegram_user_id = helpers.get_telegram_user_id(update) product = walladb.get_product({'product_name':product_name,'telegram_user_id':telegram_user_id}) if product: categories = helpers.generate_categories_string(product['category'], product['subcategory']) text = f"*Nombre del producto:* {helpers.telegram_escape_characters(product['product_name'])}\n\ *Precio desde *{helpers.telegram_escape_characters(str(product['min_price']))}€ *hasta *{helpers.telegram_escape_characters(str(product['max_price']))}€\n\ *En las coordenadas *{helpers.telegram_escape_characters(str(product['latitude']))}, {helpers.telegram_escape_characters(str(product['longitude']))} *y a *{product['distance']}km *de estas*\n\ *En las categorías: *{helpers.telegram_escape_characters(categories)}\n\ *Palabras excluídas del título: *`{helpers.telegram_escape_characters(product['title_exclude'])}`\n\ *Palabras excluídas del título y la descripción: *`{helpers.telegram_escape_characters(product['title_description_exclude'])}`" await context.bot.send_message(chat_id=update.effective_chat.id, text=text, parse_mode=ParseMode.MARKDOWN_V2) return ConversationHandler.END async def send_list(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: telegram_user_id = helpers.get_telegram_user_id(update) if walladb.is_user_active(telegram_user_id): if walladb.is_user_testing(telegram_user_id) or walladb.is_user_premium(telegram_user_id): query = update.callback_query if query.data == 'remove': text = "Escoge un producto para borrar" if query.data == 'list': text = "Escoge un producto para ver los detalles" keyboard = helpers.create_products_keyboard(telegram_user_id) markup = InlineKeyboardMarkup(keyboard) await context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=markup) async def admin_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if helpers.is_user_admin(update.message.chat_id): await update.message.reply_markdown_v2(helpers.telegram_escape_characters("¡Eres admin!")) async def add_premium_user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if helpers.is_user_admin(update.message.chat_id): 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): await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"{telegram_user_id} re-activado hasta {until}. Re-activando productos.")) else: await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"{telegram_user_id} añadido hasta {until}.")) async def remove_user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if helpers.is_user_admin(update.message.chat_id): telegram_user_id = update.message.text.split('/remove_user ')[1] walladb.remove_valid_user(telegram_user_id) await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"{telegram_user_id} desactivado.")) async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: telegram_user_id = helpers.get_telegram_user_id(update) if walladb.is_user_active(telegram_user_id): type = walladb.get_user_type(telegram_user_id) until = walladb.get_user_until(telegram_user_id) message = f"Tu cuenta es tipo: {type} y caduca el {helpers.get_spanish_date(until)}." await update.message.reply_markdown_v2(helpers.telegram_escape_characters(message)) async def start_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): message = f"Bienvenido a @wallamanta_homelabs_vip, usa el comando /help para más información." if telegram_user_id < 0: message = "Este bot no se puede usar en grupos." elif walladb.is_user_premium(telegram_user_id): message = f"Bienvenido a @wallamanta_homelabs_vip, usa el comando /help para más información." await update.message.reply_markdown_v2(helpers.telegram_escape_characters(message)) 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) await context.bot.send_message(chat_id=update.effective_chat.id, text=f"¡*{helpers.telegram_escape_characters(context.user_data['product_name'])}* añadido correctamente\!", parse_mode=ParseMode.MARKDOWN_V2) 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.") async def list_threads(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if helpers.is_user_admin(update.message.chat_id): global SEARCH_THREADS_LIST tmp_search_threads_list = [] threads_string = "" for thread in SEARCH_THREADS_LIST: if thread[1].is_alive(): tmp_search_threads_list.append((thread[0], thread[1])) threads_string = threads_string + f"{thread[0]['product_name']} - {thread[0]['telegram_user_id']}\n" SEARCH_THREADS_LIST = tmp_search_threads_list if len(threads_string) > 2000: my_strings = [(threads_string[i:i+2000]) for i in range(0, len(threads_string), 2000)] for my_string in my_strings: await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"{my_string}")) else: await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"{threads_string}")) async def message_to_all(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if helpers.is_user_admin(update.message.chat_id): message = update.message.text.split('/message_to_all ')[1] helpers.send_message_to_all(message) await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"Messages sent to all users")) async def code(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: code = update.message.text.split('/code ')[1] telegram_user_id = update.message.chat_id if len(code) != 12: await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"El código no tiene el formato correcto")) else: code = walladb.get_code(code) if code: until = helpers.get_date_ahead(code['days']) try: if code['type'] == 'premium': walladb.add_premium_user(telegram_user_id, until) await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"Activado periodo premium hasta el {until}")) elif code['type'] == 'testing': walladb.add_test_user(telegram_user_id, until) await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"Activado periodo testing hasta el {until}")) walladb.use_code(code) except Exception as e: error_code = helpers.get_random_string(8) logging.info(f"Error trying to checkout code {code}: {e}. {error_code}") await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"Ha habido un error, contacta con @jocarduck y dale el siguiente código de error: `{error_code}`")) else: await update.message.reply_markdown_v2(helpers.telegram_escape_characters(f"El código no es válido o ya ha sido usado")) def count_threads(): time.sleep(10) while True: logging.info(f"=== There are: {threading.active_count()} threads. ===") time.sleep(60) def main()->None: walladb.setup_db() """Start the bot.""" # Create the Application and pass it your bot's token. if constants.telegram_proxy: logging.info("Creating application with socks5 proxy") application = ApplicationBuilder().get_updates_http_version('1.1').http_version('1.1').token(constants.TELEGRAM_TOKEN).proxy(constants.proxy_url).get_updates_proxy(constants.proxy_url).build() else: logging.info("Creating application without socks5 proxy") application = ApplicationBuilder().get_updates_http_version('1.1').http_version('1.1').token(constants.TELEGRAM_TOKEN).build() p = threading.Thread(target=search_manager.work) p.start() p = threading.Thread(target=account_checker.work, args=(3600, )) p.start() #p = threading.Thread(target=count_threads) #p.start() # on different commands - answer in Telegram application.add_handler(CommandHandler("start", start_command)) application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("admin", admin_command)) application.add_handler(CommandHandler("add_premium_user", add_premium_user_command)) application.add_handler(CommandHandler("remove_user", remove_user_command)) application.add_handler(CommandHandler("status", status_command)) application.add_handler(CommandHandler("list_threads", list_threads)) application.add_handler(CommandHandler("message_to_all", message_to_all)) application.add_handler(CommandHandler("code", code)) #application.add_handler(CallbackQueryHandler("list", send_list())) #application.add_handler(CallbackQueryHandler(pattern="list", callback=send_list())) conv_handler = ConversationHandler( conversation_timeout=120, entry_points=[CommandHandler("menu", main_menu)], states={ ACTION: [CallbackQueryHandler(menu_click_handler)], ADD_PRODUCT_NAME: [MessageHandler(filters.TEXT, add_product_name), CallbackQueryHandler(add_product_name)], ADD_PRODUCT_MIN_PRICE: [MessageHandler(filters.TEXT, add_product_min_price)], ADD_PRODUCT_MAX_PRICE: [MessageHandler(filters.TEXT, add_product_max_price)], ADD_PRODUCT_CATEGORY: [CallbackQueryHandler(add_product_category)], CONTINUE_OR_FINISH: [CallbackQueryHandler(continue_or_finish), MessageHandler(filters.TEXT, continue_or_finish)], REMOVE_PRODUCT: [CallbackQueryHandler(remove_product)], LIST: [CallbackQueryHandler(product_details)], ConversationHandler.TIMEOUT: [CallbackQueryHandler(conv_timeout), MessageHandler(filters.TEXT, conv_timeout)], ConversationHandler.END: [MessageHandler(filters.TEXT, conv_finish)], }, fallbacks=[CommandHandler("cancel", cancel)], ) application.add_handler(conv_handler) application.add_error_handler(error) application.run_polling() if __name__ == "__main__": main()