diff --git a/.gitignore b/.gitignore index 31a25a0..fcae9c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ .env -/data/db/* \ No newline at end of file +/data/* \ No newline at end of file diff --git a/conjuntasbot/conjuntasbot.py b/conjuntasbot/conjuntasbot.py index f603303..57121f1 100644 --- a/conjuntasbot/conjuntasbot.py +++ b/conjuntasbot/conjuntasbot.py @@ -3,12 +3,14 @@ import sqlite3 import os import time import re +import gspread -from telegram import Update, ReplyKeyboardMarkup -from telegram.ext import Application, CommandHandler, MessageHandler, filters, ConversationHandler, CallbackContext +from oauth2client.service_account import ServiceAccountCredentials +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, filters, ConversationHandler, CallbackContext, CallbackQueryHandler from telegram.constants import ParseMode -PRODUCT_NAME, PRODUCT_DESCRIPTION, PRODUCT_IMAGE, LIMIT, LIMIT_PER_USER, UNLIMITED, LIMITED = range(7) +PRODUCT_NAME, PRODUCT_DESCRIPTION, PRICE_MEMBER, PRICE, PRODUCT_IMAGE, LIMIT, LIMIT_PER_USER, UNLIMITED, LIMITED = range(9) # Enable logging logging.basicConfig( @@ -24,6 +26,7 @@ httpx_logger.setLevel(logging.WARNING) admin_ids = [int(admin_id) for admin_id in os.environ.get("ADMIN_IDS", "").split(",")] group_chat_id = os.environ.get("GROUP_CHAT_ID") bot_token = os.environ.get("TELEGRAM_TOKEN") +spreadsheet_id = os.environ.get("SPREADSHEET_ID") # Configura la base de datos SQLite conn = sqlite3.connect('/app/data/db/conjuntas.db') @@ -35,6 +38,8 @@ cursor.execute('''CREATE TABLE IF NOT EXISTS conjuntas ( product_description TEXT, limite INTEGER, limit_per_user INTEGER, + price INTEGER, + price_member INTEGER, closed INTEGER, photo_id TEXT )''') @@ -49,6 +54,17 @@ cursor.execute('''CREATE TABLE IF NOT EXISTS conjunta_users ( )''') conn.commit() +# Configuramos API de Google Sheets + +json_keyfile = '/app/data/creds.json' +scopes = [ + 'https://www.googleapis.com/auth/spreadsheets', + 'https://www.googleapis.com/auth/drive' +] +creds = ServiceAccountCredentials.from_json_keyfile_name(json_keyfile, scopes) +client = gspread.authorize(creds) +spreadsheet = client.open_by_key(spreadsheet_id) + async def list_conjuntas(update: Update, context: CallbackContext): chat_id = update.message.chat_id cursor.execute("SELECT * FROM conjuntas WHERE closed=0") @@ -57,7 +73,7 @@ async def list_conjuntas(update: Update, context: CallbackContext): if conjuntas: for conjunta in conjuntas: time.sleep(0.5) - conjunta_id, message_id, product_name, product_description, limit, limit_per_user, closed, photo_id = conjunta + conjunta_id, message_id, product_name, product_description, limit, limit_per_user, price, price_member, closed, photo_id = conjunta caption = f"Conjunta para '{product_name}'\n" cursor.execute("SELECT COUNT(*) FROM conjunta_users WHERE conjunta_id = ?", (conjunta[0],)) users = cursor.fetchone() @@ -101,6 +117,34 @@ async def product_description(update: Update, context: CallbackContext): context.user_data['product_description'] = product_description + await update.message.reply_text("Envía el precio para SOCIOS.") + + return PRICE_MEMBER + +# Función para manejar el precio para socios del producto +async def product_price_member(update: Update, context: CallbackContext): + price_member = update.message.text + + try: + context.user_data['price_member'] = int(price_member) + except ValueError: + await update.message.reply_text("Envía un número, por favor.") + return PRICE_MEMBER + + await update.message.reply_text("Envía el precio para NO socios.") + + return PRICE + +# Función para manejar el precio para no socios del producto +async def product_price(update: Update, context: CallbackContext): + price = update.message.text + + try: + context.user_data['price'] = int(price) + except ValueError: + await update.message.reply_text("Envía un número, por favor.") + return PRICE + await update.message.reply_text("Envía una foto para esta conjunta.") return PRODUCT_IMAGE @@ -163,15 +207,22 @@ async def add_and_send(update: Update, context: CallbackContext, message): limit_per_user = context.user_data['limit_per_user'] photo_id = context.user_data['photo_id'] product_description = context.user_data['product_description'] + price = context.user_data['price'] + price_member = context.user_data['price_member'] sent_message = await context.bot.send_photo(group_chat_id, photo=photo_id, caption=message) - cursor.execute("INSERT INTO conjuntas (message_id, product_name, product_description, limite, limit_per_user, closed, photo_id) VALUES (?, ?, ?, ?, ?, ?, ?)", - (sent_message.message_id, product_name, product_description, limit, limit_per_user, 0, photo_id)) + cursor.execute("INSERT INTO conjuntas (message_id, product_name, product_description, price, price_member, limite, limit_per_user, closed, photo_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + (sent_message.message_id, product_name, product_description, price, price_member, limit, limit_per_user, 0, photo_id)) conn.commit() conjunta_id = cursor.lastrowid + # Creamos una nueva hoja en el documento + worksheet = spreadsheet.add_worksheet(title=f"{product_name} - {conjunta_id}", rows=1, cols=3) + #worksheet.update(values = [2, 5, 3], range_name = "A1:C1") + worksheet.append_row(["Usuario", "Cantidad", "Socio"]) + # Anclar el mensaje en el grupo await context.bot.pin_chat_message(group_chat_id, sent_message.message_id) await update_conjunta(update, context, conjunta_id) @@ -222,21 +273,35 @@ async def update_conjunta(update: Update, context: CallbackContext, conjunta_id) conjunta = cursor.fetchone() if conjunta: - conjunta_id, message_id, product_name, product_description, limit, limit_per_user, closed, photo_id = conjunta + conjunta_id, message_id, product_name, product_description, limit, limit_per_user, price, price_member, closed, photo_id = conjunta - if limit: - message = f"Conjunta para {product_name} con un límite de {limit} productos y {limit_per_user} por usuario.\n" - left = quantity_left(conjunta_id) - if left == 0: - message += f"\nYa no quedan productos disponibles.\n" + message = f"Conjunta para 🛒 {product_name}\n\n" + if limit: + message += f"#️⃣ Límite de {limit} productos.\n" + message += f"#️⃣ Límite de {limit_per_user} por usuario.\n" + left = quantity_left(conjunta_id) + if left == 0: + message += f"\n❌ Ya no quedan productos disponibles.\n" + else: + message += f"\n✅ Todavía quedan {left} productos disponibles.\n" else: - message += f"\nTodavía quedan {left} productos disponibles.\n" - else: - message = f"Conjunta para {product_name} sin límite de cantidad.\n" - message += f"\n{product_description}\n\n" - message += f"Puedes unirte respondiendo a este mensaje con me apunto {{cantidad}}, en números.\nPara borrarte responde a este mensaje y di me borro." + message = f"✅ No hay límite de productos por usuario.\n" + message += f"\n🗒️ {product_description}\n" + message += f"\n💰 Precio para socios: {price_member}€\n" + message += f"💰 Precio para NO socios: {price}€\n" + message += f"\nPuedes unirte respondiendo a este mensaje con me apunto {{cantidad}}, en números.\n" + message += f"Para borrarte responde a este mensaje y di me borro.\n" - await context.bot.edit_message_caption(chat_id=group_chat_id, message_id=message_id, caption=message, parse_mode=ParseMode.HTML) + cursor.execute("SELECT * FROM conjunta_users WHERE conjunta_id=?", (conjunta_id,)) + users = cursor.fetchall() + + if users: + message += f"\n🧍 Lista de apuntados:\n\n" + for user in users: + id, conjunta_id, user_id, user_name, quantity = user + message += f"@{user_name} - {quantity} unidades\n" + + await context.bot.edit_message_caption(chat_id=group_chat_id, message_id=message_id, caption=message, parse_mode=ParseMode.HTML) # Función para unirse a una conjunta @@ -250,7 +315,7 @@ async def handle_conjunta(update: Update, context: CallbackContext): conjunta = cursor.fetchone() if conjunta: - conjunta_id, message_id, product_name, product_description, limit, limit_per_user, closed, photo_id = conjunta + conjunta_id, message_id, product_name, product_description, limit, limit_per_user, price, price_member, closed, photo_id = conjunta regex_borrar = r'\b(?!apunto)(desapunto|borro|desapuntar|borrar)\b' regex_apuntar = r'\b(apunto|me uno)\b' @@ -264,6 +329,12 @@ async def handle_conjunta(update: Update, context: CallbackContext): if user_conjunta: cursor.execute("DELETE FROM conjunta_users WHERE id = ?", (user_conjunta[0],)) conn.commit() + + worksheet = spreadsheet.worksheet(f"{product_name} - {conjunta_id}") + found_cell = worksheet.find(user_name) + if found_cell: + worksheet.delete_rows(found_cell.row) + await update.message.reply_text("¡Desapuntado de la conjunta!") try: await update_conjunta(update, context, conjunta_id) @@ -299,6 +370,13 @@ async def handle_conjunta(update: Update, context: CallbackContext): cursor.execute("INSERT INTO conjunta_users (conjunta_id, user_id, user_name, quantity) VALUES (?, ?, ?, ?)", (conjunta_id, user_id, user_name, quantity)) conn.commit() + + socios_worksheet = spreadsheet.worksheet("Socios") + socio = socios_worksheet.find(user_name) + + worksheet = spreadsheet.worksheet(f"{product_name} - {conjunta_id}") + worksheet.append_row([user_name, quantity, "SÍ" if socio else "NO"]) + await update.message.reply_text(f"Te has unido a la conjunta para '{product_name}' con {quantity} unidades.") try: await update_conjunta(update, context, conjunta_id) @@ -313,14 +391,13 @@ async def handle_conjunta(update: Update, context: CallbackContext): # Función para consultar el estado de una conjunta async def status_conjunta(update: Update, context: CallbackContext): user_id = update.effective_user.id - chat_id = update.message.chat_id conjunta_id = context.args[0] cursor.execute("SELECT * FROM conjuntas WHERE id=?", (conjunta_id,)) conjunta = cursor.fetchone() if conjunta: - conjunta_id, message_id, product_name, product_description, limit, limit_per_user, closed, photo_id = conjunta + conjunta_id, message_id, product_name, product_description, limit, limit_per_user, price, price_member, closed, photo_id = conjunta cursor.execute("SELECT * FROM conjunta_users WHERE conjunta_id=?", (conjunta_id,)) users_data = cursor.fetchall() @@ -341,25 +418,43 @@ async def status_conjunta(update: Update, context: CallbackContext): async def close_conjunta(update: Update, context: CallbackContext): user_id = update.effective_user.id - conjunta_id = context.args[0] + query = update.callback_query + await query.answer(text="Cerrando conjunta") + + conjunta_id = int(query.data.split("close ")[1]) cursor.execute("SELECT * FROM conjuntas WHERE id=?", (conjunta_id,)) conjunta = cursor.fetchone() if conjunta: - conjunta_id, message_id, product_name, product_description, limit, limit_per_user, closed, photo_id = conjunta + conjunta_id, message_id, product_name, product_description, limit, limit_per_user, price, price_member, closed, photo_id = conjunta if user_id in admin_ids: cursor.execute("UPDATE conjuntas SET closed=1 WHERE id=?", (conjunta_id,)) conn.commit() - await update.message.reply_text(f"La conjunta para '{product_name}' ha sido cerrada.") + message = f"La conjunta para {product_name} ha sido cerrada.\n" + message += f"La lista de apuntados es la siguiente:\n\n" + + cursor.execute("SELECT * FROM conjunta_users WHERE conjunta_id = ?", (conjunta_id,)) + + for user in cursor.fetchall(): + id, conjunta_id, user_id, user_name, quantity = user + message += f"@{user_name},{quantity}\n" + + message += "" + + message += f"\n\n🗒️ {product_description}\n" + message += f"\n💰 Precio para socios: {price_member}€\n" + message += f"💰 Precio para NO socios: {price}€\n" + + await context.bot.send_message(chat_id=group_chat_id, text=message, parse_mode=ParseMode.HTML) # Desanclamos el mensaje - #context.bot.unpin_chat_message(chat_id) + await context.bot.unpin_chat_message(group_chat_id, message_id) else: - await update.message.reply_text("Solo el administrador puede cerrar la conjunta.") + await context.bot.send_message(chat_id=group_chat_id, text="Solo el administrador puede cerrar la conjunta.") else: - await update.message.reply_text("La conjunta no existe.") + await context.bot.send_message(chat_id=group_chat_id, text="La conjunta no existe.") # Función para obtener un resumen de las conjuntas activas async def admin_summary(update: Update, context: CallbackContext): @@ -372,7 +467,7 @@ async def admin_summary(update: Update, context: CallbackContext): if conjuntas: summary_text = "Resumen de conjuntas activas:\n\n" for conjunta in conjuntas: - conjunta_id, message_id, product_name, product_description, limit, limit_per_user, closed, photo_id = conjunta + conjunta_id, message_id, product_name, product_description, limit, limit_per_user, price, price_member, closed, photo_id = conjunta cursor.execute("SELECT COUNT(*) FROM conjunta_users WHERE conjunta_id=?", (conjunta_id,)) num_users = cursor.fetchone()[0] cursor.execute("SELECT quantity FROM conjunta_users WHERE conjunta_id=?", (conjunta_id,)) @@ -380,10 +475,12 @@ async def admin_summary(update: Update, context: CallbackContext): if num_users: for quantity in cursor.fetchall(): total_quantity += quantity[0] - summary_text += f"ID: {conjunta_id}\n" - summary_text += f"Producto: {product_name}\n" - summary_text += f"Usuarios apuntados: {num_users}\n" - summary_text += f"Cantidad total de productos pedidos: {total_quantity}\n" + summary_text += f"🛒 {product_name} ({conjunta_id})\n" + summary_text += f"🧍 Usuarios apuntados: {num_users}\n" + if limit: + summary_text += f"#️⃣ Límite: {limit}\n" + summary_text += f"#️⃣ Límite por usuario: {limit_per_user}\n" + summary_text += f"🔢 Cantidad total de productos pedidos: {total_quantity}\n" summary_text += f"\n--------------------------------------\n\n" await update.message.reply_text(summary_text, parse_mode=ParseMode.HTML) @@ -391,9 +488,15 @@ async def admin_summary(update: Update, context: CallbackContext): # Agregar botones para seleccionar una conjunta keyboard = [] + conjuntas_line = [] + count = 0 for conjunta in conjuntas: - keyboard.append([str(conjunta[0])]) - reply_markup = ReplyKeyboardMarkup(keyboard, one_time_keyboard=True) + conjunta_id, message_id, product_name, product_description, limit, limit_per_user, price, price_member, closed, photo_id = conjunta + conjuntas_line.append(InlineKeyboardButton(f"ℹ️ {product_name} ({str(conjunta_id)})", callback_data=f"info {str(conjunta_id)}")) + conjuntas_line.append(InlineKeyboardButton(f"❌ {product_name} ({str(conjunta_id)})", callback_data=f"close {str(conjunta_id)}")) + keyboard.append(conjuntas_line) + conjuntas_line = [] + reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text("Selecciona una conjunta para ver más detalles.", reply_markup=reply_markup) else: await update.message.reply_text("No hay conjuntas activas en este grupo.") @@ -402,29 +505,33 @@ async def admin_summary(update: Update, context: CallbackContext): # Función para mostrar detalles de una conjunta seleccionada async def show_conjunta_details(update: Update, context: CallbackContext): - if "conjuntas" in context.user_data: - selected_conjunta_idx = int(update.message.text) - - cursor.execute("SELECT * FROM conjuntas WHERE id = ?", (selected_conjunta_idx,)) - conjunta = cursor.fetchone() - conjunta_id, message_id, product_name, product_description, limit, limit_per_user, closed, photo_id = conjunta - - selected_conjunta_details = f"Detalles de la conjunta seleccionada:\n" - selected_conjunta_details += f"Producto: {product_name}\n" - selected_conjunta_details += f"Descripción: {product_description}\n" - if limit: - selected_conjunta_details += f"Límite total: {limit}\n" - selected_conjunta_details += f"Límite por usuario: {limit_per_user}\n" - - cursor.execute("SELECT * FROM conjunta_users WHERE conjunta_id = ?", (conjunta_id,)) - - selected_conjunta_details += f"Apuntados:\n\n-----------------------\n" - for user in cursor.fetchall(): - id, conjunta_id, user_id, user_name, quantity = user - selected_conjunta_details += f"@{user_name},{quantity}\n" - selected_conjunta_details += "-----------------------" + query = update.callback_query + await query.answer(text="Mostrando detalles") - await update.message.reply_photo(photo=photo_id, caption=selected_conjunta_details, parse_mode=ParseMode.HTML) + selected_conjunta_idx = int(query.data.split("info ")[1]) + + cursor.execute("SELECT * FROM conjuntas WHERE id = ?", (selected_conjunta_idx,)) + conjunta = cursor.fetchone() + conjunta_id, message_id, product_name, product_description, limit, limit_per_user, price, price_member, closed, photo_id = conjunta + + selected_conjunta_details = f"Detalles de la conjunta seleccionada:\n" + selected_conjunta_details += f"🛒 {product_name}\n" + selected_conjunta_details += f"🗒️ {product_description}\n" + if limit: + selected_conjunta_details += f"#️⃣ Límite: {limit}\n" + selected_conjunta_details += f"#️⃣ Límite por usuario: {limit_per_user}\n" + selected_conjunta_details += f"💰 Precio para socios: {price_member}€\n" + selected_conjunta_details += f"💰 Precio para NO socios: {price}€\n" + + cursor.execute("SELECT * FROM conjunta_users WHERE conjunta_id = ?", (conjunta_id,)) + + selected_conjunta_details += f"\n🧍 Usuarios apuntados:\n\n-----------------------\n" + for user in cursor.fetchall(): + id, conjunta_id, user_id, user_name, quantity = user + selected_conjunta_details += f"@{user_name},{quantity}\n" + selected_conjunta_details += "-----------------------" + + await context.bot.send_photo(chat_id=update.effective_chat.id, photo=photo_id, caption=selected_conjunta_details, parse_mode=ParseMode.HTML) def main()->None: application = Application.builder().get_updates_http_version('1.1').http_version('1.1').token(bot_token).build() @@ -434,6 +541,8 @@ def main()->None: states={ PRODUCT_NAME: [MessageHandler(filters.TEXT, product_name)], PRODUCT_DESCRIPTION: [MessageHandler(filters.TEXT, product_description)], + PRICE_MEMBER: [MessageHandler(filters.TEXT, product_price_member)], + PRICE: [MessageHandler(filters.TEXT, product_price)], PRODUCT_IMAGE: [MessageHandler(filters.PHOTO, product_image)], LIMIT: [MessageHandler(filters.TEXT, limit)], LIMIT_PER_USER: [MessageHandler(filters.TEXT, limit_per_user)], @@ -445,13 +554,15 @@ def main()->None: application.add_handler(conv_handler) application.add_handler(CommandHandler('status', status_conjunta)) - application.add_handler(CommandHandler('close', close_conjunta)) + application.add_handler(CallbackQueryHandler(close_conjunta, pattern="close \d")) application.add_handler(CommandHandler('list', list_conjuntas)) application.add_handler(CommandHandler('admin_summary', admin_summary)) - application.add_handler(MessageHandler(filters.TEXT & filters.Regex(r'^\d+$'), show_conjunta_details)) + application.add_handler(CallbackQueryHandler(show_conjunta_details, pattern="info \d")) application.add_handler(MessageHandler(filters.REPLY, handle_conjunta)) application.run_polling() + conn.close() + if __name__ == '__main__': main() diff --git a/conjuntasbot/requirements.txt b/conjuntasbot/requirements.txt index 2616ef4..8b72fe5 100644 --- a/conjuntasbot/requirements.txt +++ b/conjuntasbot/requirements.txt @@ -1 +1,3 @@ -python-telegram-bot==20.4 \ No newline at end of file +python-telegram-bot==20.4 +gspread==5.12.0 +oauth2client==4.1.3 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 5b4767d..34e5742 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,5 +18,6 @@ services: - NR_HOST_METRICS=${NR_HOST_METRICS} - ADMIN_IDS=${ADMIN_IDS} - GROUP_CHAT_ID=${GROUP_CHAT_ID} + - SPREADSHEET_ID=${SPREADSHEET_ID} dns: - 8.8.8.8 \ No newline at end of file