diff --git a/home/views.py b/home/views.py deleted file mode 100644 index b18f3e9..0000000 --- a/home/views.py +++ /dev/null @@ -1,3182 +0,0 @@ -import socket, hashlib, gmpy2, binascii, os, re, json, logging, secrets -from django.shortcuts import render, redirect -from django.urls import reverse -from django.http import HttpResponse, JsonResponse -from django.db import connections -from django.contrib import messages -from django.contrib.auth import logout -from .pagos_sumup import crear_checkout, crear_checkout_battlepay, obtener_checkout -from django import forms -import stripe -from .pagos_stripe import create_checkout_session, is_session_paid -from functools import wraps -from .models import Noticia, ClienteCategoria, ServerSelection, RecruitAFriend, DownloadClientPage, ContentCreator, RecruitReward, ClaimedReward, AccountActivation, SecurityToken, GuildRenameSettings, VoteSite, VoteLog, HomeApiPoints, UnstuckHistory, ReviveHistory, RenamePrice, CustomizePrice, ChangeRacePrice, ChangeFactionPrice, LevelUpPrice, GoldPrice, TransferPrice, Category, Item, StripeLog, LoginAttempt, Pedido - -from django.views.decorators.csrf import csrf_exempt -from datetime import datetime, timedelta -from .zone_definitions import get_zone_name -from .ac_soap import execute_soap_command -from . import bnet -from django.utils import timezone -from .library_correo import enviar_correo -from django.utils.crypto import get_random_string -from django.conf import settings -from django.utils.timezone import now -from decimal import Decimal, ROUND_HALF_UP - -# Configuración del logger -logger = logging.getLogger(__name__) - - -def require_paid_stripe(redirect_to='index'): - """ - Protege las vistas de "éxito" de pago: exige un `session_id` de Stripe - realmente PAGADO en la URL (Stripe lo añade tras el pago) y evita re-entregas - marcando el StripeLog como `fulfilled`. Cierra el bypass de ir directo a la - success-url sin pagar. - """ - def deco(view): - @wraps(view) - def wrapper(request, *args, **kwargs): - session_id = request.GET.get('session_id') - if not is_session_paid(session_id): - messages.error(request, 'No se ha podido verificar el pago.') - return redirect(redirect_to) - log = StripeLog.objects.filter(session_id=session_id).first() - if log is not None and log.fulfilled: - messages.info(request, 'Este pago ya había sido procesado.') - return redirect(redirect_to) - response = view(request, *args, **kwargs) - if log is not None: - log.fulfilled = True - log.save(update_fields=['fulfilled']) - return response - return wrapper - return deco - -@csrf_exempt -def stripe_webhook(request): - payload = request.body - sig_header = request.META.get("HTTP_STRIPE_SIGNATURE") - - try: - event = stripe.Webhook.construct_event(payload, sig_header, settings.STRIPE_WEBHOOK_SECRET) - - if event["type"] == "checkout.session.completed": - session = event["data"]["object"] - session_id = session["id"] - stripe_ip = request.META.get("REMOTE_ADDR") # IP detectada - - # 🔹 Actualizar la IP de Stripe en el log - StripeLog.objects.filter(session_id=session_id).update(stripe_ip=stripe_ip, timestamp=now()) - - return JsonResponse({"success": True}) - - except stripe.error.SignatureVerificationError: - return JsonResponse({"success": False, "message": "Firma de webhook no válida."}, status=400) - -def get_class_css(class_id): - class_map = { - 1: 'warrior', - 2: 'paladin', - 3: 'hunter', - 4: 'rogue', - 5: 'priest', - 6: 'death-knight', - 7: 'shaman', - 8: 'mage', - 9: 'warlock', - 11: 'druid' - } - return class_map.get(class_id, 'unknown') - -def get_class_text_css(class_id): - text_class_map = { - 1: 'warrior', - 2: 'paladin', - 3: 'hunter', - 4: 'rogue', - 5: 'priest', - 6: 'death-knight', - 7: 'shaman', - 8: 'mage', - 9: 'warlock', - 11: 'druid' - } - return text_class_map.get(class_id, 'unknown') - -def get_character_image(class_id, race, gender): - """ - Devuelve la imagen del personaje basada en la raza, clase y género. - `gender`: 0 = Hombre, 1 = Mujer - """ - # Definir los nombres de archivos según el género - gender_suffix = 'male' if gender == 0 else 'female' - - # Raza y clases compatibles - race_class_map = { - 10: [2, 3, 4, 5, 6, 8, 9], # Elfo de Sangre - 8: [1, 3, 4, 5, 6, 7, 8], # Trol - 6: [1, 3, 6, 7, 11], # Tauren - 5: [1, 4, 5, 6, 8, 9], # No-Muerto - 2: [1, 3, 4, 6, 7, 9], # Orco - 11: [1, 2, 3, 5, 6, 7, 8], # Draenei - 7: [1, 4, 6, 8, 9], # Gnomo - 4: [1, 3, 4, 5, 6, 11], # Elfo de la Noche - 3: [1, 2, 3, 4, 5, 6], # Enano - 1: [1, 2, 4, 5, 6, 8, 9] # Humano - } - - # Verificar si la clase es compatible con la raza - if class_id not in race_class_map.get(race, []): - return 'nw-themes/nw-ryu/nw-images/nw-classes/unknown.webp' # Imagen por defecto si no es compatible - - # Construir el nombre del archivo basado en la raza y el género - race_image_map = { - 10: f"big-blood-elf-{gender_suffix}.webp", - 8: f"big-troll-{gender_suffix}.webp", - 6: f"big-tauren-{gender_suffix}.webp", - 5: f"big-undead-{gender_suffix}.webp", - 2: f"big-orc-{gender_suffix}.webp", - 11: f"big-draenei-{gender_suffix}.webp", - 7: f"big-gnome-{gender_suffix}.webp", - 4: f"big-night-elf-{gender_suffix}.webp", - 3: f"big-dwarf-{gender_suffix}.webp", - 1: f"big-human-{gender_suffix}.webp" - } - - # Devolver la imagen según la raza y género, o una imagen por defecto - return f"nw-themes/nw-ryu/nw-images/nw-races/{race_image_map.get(race, 'unknown.webp')}" - - - -dias_semana = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'] -meses = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] - -def formatear_fecha(fecha): - """Formatea la fecha al estilo solicitado.""" - dia_semana = dias_semana[fecha.weekday()] - mes = meses[fecha.month - 1] - return f"{dia_semana} {fecha.day:02d} de {mes} del {fecha.year}" - - -# Clase de formulario de inicio de sesión (Battle.net: login por email) -class LoginForm(forms.Form): - email = forms.EmailField(max_length=320, required=True) - password = forms.CharField(widget=forms.PasswordInput, required=True) - -def sha1(data): - """Función auxiliar para calcular SHA-1.""" - return hashlib.sha1(data).digest() - -def calculate_srp6_verifier(username, password, salt): - """Calcula el verifier usando SRP6 basado en la implementación PHP.""" - g = gmpy2.mpz(7) - N = gmpy2.mpz('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 16) - - # Paso 1: Calcular H(username:password) - h1 = sha1((username.upper() + ':' + password.upper()).encode('utf-8')) - - # Usar el salt proporcionado (sin invertir) - salt_bytes = bytes.fromhex(salt) - - # Paso 2: Calcular H(salt + H(username:password)) - h2 = sha1(salt_bytes + h1) - - # Convertir el hash a un entero (little-endian) - h2_int = gmpy2.mpz.from_bytes(h2, 'little') - - # Calcular el verifier usando g^h2 mod N - verifier = gmpy2.powmod(g, h2_int, N) - verifier_bytes = verifier.to_bytes(32, 'little') - - # Convertir el resultado a hexadecimal en mayúsculas - return binascii.hexlify(verifier_bytes).decode('utf-8').upper() - -def authenticate(email, password): - """Autentica una cuenta Battle.net por email (SRP6 v2).""" - try: - with connections['acore_auth'].cursor() as cursor: - account = bnet.get_bnet_account_by_email(cursor, email) - if not account: - return None - if bnet.bnet_verify(account['email'], password, account['salt'], account['verifier']): - return account - return None - except Exception: - return None - - -def _set_game_account_session(request, game_account): - """Fija en sesión la cuenta de juego seleccionada (para las demás vistas).""" - request.session['username'] = game_account['username'].upper() - request.session['account_id'] = game_account['id'] - - -def login_view(request): - if request.method == 'POST': - form = LoginForm(request.POST) - if form.is_valid(): - email = form.cleaned_data['email'] - password = form.cleaned_data['password'] - ip_address = request.META.get('REMOTE_ADDR') - - account = authenticate(email, password) - if account: - log_login_attempt(email, "Exito", ip_address) - - # Guardar la identidad Battle.net en la sesión - request.session['bnet_id'] = account['id'] - request.session['bnet_email'] = account['email'] - - # Resolver las cuentas de juego enlazadas - with connections['acore_auth'].cursor() as cursor: - game_accounts = bnet.get_game_accounts_for_bnet(cursor, account['id']) - - if len(game_accounts) == 1: - # Una sola cuenta de juego: seleccionar automáticamente - _set_game_account_session(request, game_accounts[0]) - elif len(game_accounts) > 1: - # Varias: dejar la selección pendiente (my-account redirige al selector) - request.session.pop('username', None) - request.session.pop('account_id', None) - # 0 cuentas: se queda sin username; el selector mostrará el aviso - - return JsonResponse({'success': True}) - else: - log_login_attempt(email, "Fracaso", ip_address) - return JsonResponse({'success': False, 'error': "Correo o contraseña incorrectos"}) - else: - return JsonResponse({'success': False, 'error': "Formulario no valido"}) - - form = LoginForm() - return render(request, 'auth/login.html', {'form': form}) - - -def select_account_view(request): - """Selector de cuenta de juego para cuentas Battle.net con varias.""" - bnet_id = request.session.get('bnet_id') - if not bnet_id: - return redirect('login') - - with connections['acore_auth'].cursor() as cursor: - game_accounts = bnet.get_game_accounts_for_bnet(cursor, bnet_id) - - if request.method == 'POST': - try: - selected_id = int(request.POST.get('account_id', '')) - except (TypeError, ValueError): - selected_id = None - chosen = next((g for g in game_accounts if g['id'] == selected_id), None) - if chosen: - _set_game_account_session(request, chosen) - return redirect('my-account') - messages.error(request, 'Cuenta de juego no válida.') - - # Autoseleccionar si solo hay una - if len(game_accounts) == 1: - _set_game_account_session(request, game_accounts[0]) - return redirect('my-account') - - return render(request, 'auth/select_account.html', { - 'game_accounts': game_accounts, - 'bnet_email': request.session.get('bnet_email'), - }) -# Función para contar los personajes en línea -def get_online_characters_count(): - with connections['acore_characters'].cursor() as cursor: - cursor.execute("SELECT COUNT(*) FROM characters WHERE online = 1") - result = cursor.fetchone() - return result[0] if result else 0 - -# Verificar si un servidor está en línea -def check_server_status(host, port): - if not host or host == "N/A": - return False - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1) - try: - s.connect((host, port)) - s.close() - return True - except (socket.timeout, ConnectionRefusedError, socket.gaierror): - return False - -# Vista principal que muestra noticias y estado del servidor -def home_view(request): - noticias = Noticia.objects.order_by('-fecha_publicacion')[:50] - online_characters = get_online_characters_count() - server_selection = ServerSelection.objects.first() - status = check_server_status(server_selection.address, server_selection.port) if server_selection else None - game_version = 'WotLK' if server_selection and server_selection.gamebuild == 12340 else 'Cataclysm' - - context = { - 'noticias': noticias, - 'server': server_selection, - 'status': 'Online' if status else 'Offline', - 'expansion': game_version, - 'online_characters': online_characters, - } - return render(request, 'home/home.html', context) - -# Vista para descargar el cliente -def download_client_view(request): - download_content = DownloadClientPage.objects.first() - categorias = ClienteCategoria.objects.all() - return render(request, 'download/download_client.html', { - 'download_content': download_content, - 'categorias': categorias, - }) - -# Vista para descargar addons -def download_addons_view(request): - return redirect("https://foro.novawow.com/files/") - -# Vista para mostrar creadores de contenido -def content_creators_view(request): - content_creator = ContentCreator.objects.first() - return render(request, 'community/content_creators.html', {'content_creator': content_creator}) - -# Vista para mostrar el estado del servidor -def server_status_view(request): - server_selection = ServerSelection.objects.first() - if server_selection: - status = check_server_status(server_selection.address, server_selection.port) - game_version = 'WotLK' if server_selection.gamebuild == 12340 else 'Cataclysm' - else: - status = None - game_version = None - - return render(request, 'server_status/server_status.html', { - 'server': server_selection, - 'status': 'Online' if status else 'Offline', - 'game_version': game_version, - }) - - -# Vista para cerrar sesión -def logout_view(request): - for key in ('username', 'account_id', 'bnet_id', 'bnet_email'): - request.session.pop(key, None) - return redirect('index') - -# Vistas adicionales -def register_view(request): - if request.method == 'POST': - password = request.POST.get('password', '').strip() - conf_password = request.POST.get('conf-password', '').strip() - email = request.POST.get('email', '').strip() - conf_email = request.POST.get('conf-email', '').strip() - recruiter = request.POST.get('recruiter', '').strip() - - # En 3.4.3 la identidad es el email (cuenta Battle.net); no hay username. - if not password or not email: - return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) - - if password != conf_password: - return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'}) - - if len(password) > 16: - return JsonResponse({'success': False, 'message': 'La contraseña no debe exceder los 16 caracteres.'}) - - if email != conf_email or not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', email): - return JsonResponse({'success': False, 'message': 'El correo electrónico no es válido.'}) - - # Verificar si ya existe una cuenta Battle.net con ese email - email_norm = bnet.normalize_email(email) - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT COUNT(*) FROM battlenet_accounts WHERE email = %s", [email_norm]) - if cursor.fetchone()[0] > 0: - return JsonResponse({'success': False, 'message': 'Ya existe una cuenta con ese correo electrónico.'}) - - # Comprobar que no haya un registro de activación pendiente para ese email - if AccountActivation.objects.filter(email=email, old_email__isnull=True).exists(): - AccountActivation.objects.filter(email=email, old_email__isnull=True).delete() - - # Validar y obtener recruiter_id (id de cuenta de juego del reclutador) - recruiter_id = 0 - if recruiter: - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [recruiter]) - recruiter_data = cursor.fetchone() - if recruiter_data: - recruiter_id = recruiter_data[0] - else: - with connections['acore_characters'].cursor() as char_cursor: - char_cursor.execute("SELECT account FROM characters WHERE name = %s", [recruiter]) - character_data = char_cursor.fetchone() - if character_data: - recruiter_id = character_data[0] - else: - return JsonResponse({'success': False, 'message': 'El reclutador ingresado no existe.'}) - - # Generar un hash único para activación. Los salt/verifier se calculan al - # activar (tanto el de bnet como el de la cuenta de juego). - activation_hash = get_random_string(32) - - AccountActivation.objects.create( - email=email, - password=password, - recruiter_id=recruiter_id, - hash=activation_hash - ) - - # Enviar correo de activación - activation_link = f"{settings.URL_PRINCIPAL}/es/activate-account?act={activation_hash}" - context = { - 'username': email, - 'password': password, - 'activation_link': activation_link, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - enviar_correo( - subject=f'Activación de la cuenta {email} - {settings.NOMBRE_SERVIDOR}', - to_email=email, - template='emails/activation.html', - context=context - ) - - message = f""" -
- La cuenta '{email}' ha sido creada.

- Se ha enviado un enlace de activación al correo {email}. -
- """ - return JsonResponse({'success': True, 'message': message}) - - return render(request, 'auth/register.html') - - -def activate_account_view(request): - activation_hash = request.GET.get('act') - - try: - activation = AccountActivation.objects.get(hash=activation_hash) - - if activation.is_expired(): - return render(request, 'auth/activation_invalid.html') - - email_norm = bnet.normalize_email(activation.email) - password = activation.password - last_ip = request.META.get('REMOTE_ADDR') - - with connections['acore_auth'].cursor() as cursor: - # Evitar duplicados si el enlace se usa dos veces - cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [email_norm]) - existing = cursor.fetchone() - if existing: - activation.delete() - return render(request, 'auth/activation_success.html') - - # 1) Crear la cuenta Battle.net (SRP6 v2) - bnet_salt, bnet_verifier, srp_version = bnet.bnet_make_registration(email_norm, password) - cursor.execute( - "INSERT INTO battlenet_accounts (email, srp_version, salt, verifier) " - "VALUES (%s, %s, %s, %s)", - [email_norm, srp_version, bnet_salt, bnet_verifier] - ) - - # 2) Recuperar el id de la cuenta bnet recién creada - cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [email_norm]) - bnet_id = cursor.fetchone()[0] - - # 3) Crear la cuenta de juego enlazada (#1, SRP6 Grunt) - game_username = bnet.make_game_account_username(bnet_id, 1) - game_salt, game_verifier = bnet.game_make_registration(game_username, password) - cursor.execute( - "INSERT INTO account " - "(username, salt, verifier, email, reg_mail, recruiter, joindate, " - " last_ip, expansion, battlenet_account, battlenet_index) " - "VALUES (%s, %s, %s, %s, %s, %s, NOW(), %s, %s, %s, %s)", - [ - game_username, game_salt, game_verifier, - email_norm, email_norm, activation.recruiter_id or 0, - last_ip, 2, bnet_id, 1 - ] - ) - - activation.delete() - return render(request, 'auth/activation_success.html') - - except AccountActivation.DoesNotExist: - return render(request, 'auth/activation_invalid.html') - -def recover_account_view(request): - return render(request, 'auth/recover_account.html') - -def novawow_realm_view(request): - return render(request, 'realms/novawow_realm.html') - -def contact_us_view(request): - return render(request, 'contact/contact_us.html') - -def legal_notice_view(request): - return render(request, 'legal/legal_notice.html') - -def maintenance_view(request): - return render(request, 'maintenance/maintenance.html') - -def not_found(request, exception=None): - return render(request, '404.html', status=404) - -def terms_and_conditions_view(request): - return render(request, 'legal/terms_and_conditions.html') - -def privacy_policy_view(request): - return render(request, 'legal/privacy_policy.html') - -def refund_policy_view(request): - return render(request, 'legal/refund_policy.html') - -def cookies_view(request): - return render(request, 'legal/cookies.html') - -def recruit_a_friend_view(request): - username = request.session.get('username') - account_status = None - characters = [] - claimed_rewards_count = 0 - - # Si el usuario está logueado, obtener su información - if username: - # Obtener información del usuario - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, last_ip FROM account WHERE username = %s", [username]) - account_data = cursor.fetchone() - - if account_data: - account_id = account_data[0] - user_ip = account_data[1] - account_status = get_account_status(account_id) - characters = get_account_characters(account_id) - claimed_rewards_count = ClaimedReward.objects.filter(account_id=account_id).count() - else: - account_id = None - user_ip = None - else: - account_id = None - user_ip = None - - # Obtener todas las recompensas - all_rewards = RecruitReward.objects.all() - available_rewards = [] - - # Si el usuario está logueado, filtrar recompensas disponibles - if account_id: - available_rewards = [reward for reward in all_rewards if not ClaimedReward.objects.filter(account_id=account_id, recruit_reward=reward).exists()] - - # Manejar solicitudes POST (solo si el usuario está logueado) - if request.method == 'POST': - if not username: - return JsonResponse({'success': False, 'message': 'Debes estar logueado para reclamar una recompensa.'}) - - if not account_id or account_status is None: - return JsonResponse({'success': False, 'message': 'No se ha encontrado tu cuenta.'}) - - reward_id = request.POST.get('reward_id') - character_name = request.POST.get('character') - - if not reward_id or not character_name: - return JsonResponse({'success': False, 'message': 'Selecciona una recompensa y un personaje.'}) - - try: - reward = RecruitReward.objects.get(id=reward_id) - - # Verificar si ya se ha reclamado la recompensa - if ClaimedReward.objects.filter(account_id=account_id, recruit_reward=reward).exists(): - return JsonResponse({'success': False, 'message': 'Ya has reclamado esta recompensa.'}) - - # Verificar si cumple los requisitos - if account_status['recruited_level_80_count'] < reward.required_friends: - return JsonResponse({'success': False, 'message': 'No cumples con los requisitos.'}) - - # Obtener amigos reclutados que han alcanzado el nivel 80 - recruited_friends = get_recruited_friends_at_level_80(account_id, reward.required_friends) - - # Comprobar si alguno de los reclutados es válido - if len(recruited_friends) < reward.required_friends: - return JsonResponse({'success': False, 'message': 'No cumples con los requisitos o los amigos tienen la misma IP.'}) - - # Comprobar si alguno de los reclutados tiene la misma IP que el usuario - if check_recruited_ip_conflict(recruited_friends, user_ip): - return JsonResponse({'success': False, 'message': 'No puedes reclamar recompensas de amigos que tienen la misma IP.'}) - - # Ejecutar el comando SOAP - command = f".send items {character_name} 'Recompensa de Recluta Amigo' 'Tu amigo alcanzó el Nivel 80' {reward.item_id}:{reward.item_quantity}" - response = execute_soap_command(command) - - if response and "Mail sent to" in response: - # Registrar la recompensa como reclamada - ClaimedReward.objects.create( - account_id=account_id, - username=username, - recruit_reward=reward, - character_name=character_name, - claimed_at=timezone.now(), - ip_address=request.META.get('REMOTE_ADDR') - ) - claimed_rewards_count = ClaimedReward.objects.filter(account_id=account_id).count() - - return JsonResponse({ - 'success': True, - 'message': f"Recompensa '{reward.reward_name}' entregada a {character_name}.", - 'claimed_count': claimed_rewards_count - }) - else: - return JsonResponse({'success': False, 'message': 'Error al entregar la recompensa.'}) - - except RecruitReward.DoesNotExist: - return JsonResponse({'success': False, 'message': 'Recompensa no encontrada.'}) - - # Renderizar la página para usuarios no logueados y logueados - return render(request, 'recruit/recruit_a_friend.html', { - 'account_status': account_status, - 'all_rewards': all_rewards, - 'available_rewards': available_rewards, - 'characters': characters, - 'claimed_rewards_count': claimed_rewards_count, - 'is_logged_in': username is not None - }) - - -def check_recruited_ip_conflict(recruited_accounts, user_ip): - """ - Verifica si alguna de las cuentas reclutadas tiene la misma IP que la cuenta del reclutador. - """ - if not recruited_accounts: - return False - - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT last_ip FROM account WHERE id IN %s - """, [tuple(recruited_accounts)]) - recruited_ips = [ip[0] for ip in cursor.fetchall()] - - return user_ip in recruited_ips - -def get_recruited_friends_at_level_80(account_id, required_count): - """ - Obtiene una lista de IDs de amigos reclutados que han alcanzado el nivel 80 y - no tienen la misma IP que el reclutador. - """ - # Primero obtener los IDs de cuentas reclutadas desde `acore_auth` - with connections['acore_auth'].cursor() as auth_cursor: - auth_cursor.execute(""" - SELECT id, last_ip FROM account WHERE recruiter = %s - """, [account_id]) - recruited_accounts = auth_cursor.fetchall() - - if not recruited_accounts: - return [] - - # Extraer IDs y IPs de las cuentas reclutadas - recruited_ids = [acc[0] for acc in recruited_accounts] - recruited_ips = {acc[0]: acc[1] for acc in recruited_accounts} - - # Obtener la IP del usuario actual - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT last_ip FROM account WHERE id = %s", [account_id]) - user_ip = cursor.fetchone()[0] - - # Luego, verificar cuáles de esas cuentas tienen personajes al nivel 80 en `acore_characters` - with connections['acore_characters'].cursor() as char_cursor: - char_cursor.execute(""" - SELECT DISTINCT account FROM characters - WHERE level = 80 AND account IN %s - LIMIT %s - """, [tuple(recruited_ids), required_count]) - recruited_at_level_80 = [acc[0] for acc in char_cursor.fetchall()] - - # Filtrar cuentas que no tienen la misma IP que el usuario actual - valid_recruits = [acc for acc in recruited_at_level_80 if recruited_ips[acc] != user_ip] - - return valid_recruits - - -def my_account(request): - # Verificar si la sesión contiene la cuenta de juego seleccionada - username = request.session.get('username') - - # Si aún no se ha elegido cuenta de juego pero hay login Battle.net, - # enviar al selector; si no, al inicio de sesión. - if not username: - if request.session.get('bnet_id'): - return redirect('select-account') - return redirect('login') - - is_logged_in = True # Si el usuario está en la sesión, asumimos que está conectado - - # Obtener información básica de la cuenta - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, username, reg_mail, email, last_ip, last_attempt_ip, joindate - FROM account - WHERE username = %s - """, [username]) - account_data = cursor.fetchone() - - if not account_data: - return redirect('index') - - account_id = account_data[0] - - # Obtener los personajes asociados a la cuenta - characters = get_account_characters(account_id) - has_characters = bool(characters) - - # Obtener el estado de la cuenta - account_status = get_account_status(account_id) - - # Obtener los puntos (DP y VP) del usuario desde la tabla `home_api_points` - user_points = HomeApiPoints.objects.filter(accountID=account_id).first() - dp = user_points.dp if user_points else 0 - vp = user_points.vp if user_points else 0 - - - # Obtener el estado del token de seguridad - security_token = SecurityToken.objects.filter(user_id=account_id).first() - if security_token: - token_status = "Solicitado" - token_date = security_token.created_at.strftime('%H:%M:%S %d-%m-%Y') - else: - token_status = "Sin solicitar" - token_date = None - - # Créditos Battlepay de la cuenta Battle.net - with connections['acore_auth'].cursor() as cursor: - battlepay_credits = bnet.get_battlepay_credits(cursor, request.session.get('bnet_id')) - - return render(request, 'my-account/my-account.html', { - 'is_logged_in': is_logged_in, - 'battlepay_credits': battlepay_credits, - 'user_info': { - 'username': account_data[1], - 'reg_mail': account_data[2], - 'email': account_data[3], - 'last_ip': account_data[4], - 'last_attempt_ip': account_data[5], - 'joindate': formatear_fecha(account_data[6]), - - }, - 'account_status': account_status, - 'characters': characters, - 'has_characters': has_characters, - 'dp': dp, - 'vp': vp, - 'token_status': token_status, - 'token_date': token_date - }) - - -def get_account_status(account_id): - with connections['acore_auth'].cursor() as cursor: - # Obtener información de la tabla 'account' - cursor.execute(""" - SELECT username, reg_mail, email, last_ip, last_attempt_ip, joindate, recruiter - FROM account - WHERE id = %s - """, [account_id]) - account_data = cursor.fetchone() - - if not account_data: - return None - - # Verificar si la cuenta está baneada - cursor.execute(""" - SELECT unbandate, UNIX_TIMESTAMP() - FROM account_banned - WHERE id = %s AND active = 1 - """, [account_id]) - ban_data = cursor.fetchone() - - # Obtener el número de amigos reclutados - cursor.execute(""" - SELECT id - FROM account - WHERE recruiter = %s - """, [account_id]) - recruited_accounts = cursor.fetchall() - recruited_count = len(recruited_accounts) - - # Contar cuántos personajes de amigos reclutados han alcanzado el nivel 80 - if recruited_count > 0: - recruited_ids = [acc[0] for acc in recruited_accounts] - with connections['acore_characters'].cursor() as char_cursor: - char_cursor.execute(""" - SELECT COUNT(*) - FROM characters - WHERE account IN %s AND level = 80 - """, [tuple(recruited_ids)]) - level_80_count = char_cursor.fetchone()[0] - else: - level_80_count = 0 - - # Asignar los datos a variables - user_info = { - 'username': account_data[0] if account_data[0] else 'No disponible', - 'reg_mail': account_data[1] if account_data[1] else 'No disponible', - 'email': account_data[2] if account_data[2] else 'No disponible', - 'last_ip': account_data[3] if account_data[3] else 'Nunca', - 'last_attempt_ip': account_data[4] if account_data[4] else 'Nunca', - 'joindate': account_data[5].strftime("%A %d de %B del %Y a las %H:%M:%S Horas") if account_data[5] else 'No disponible', - 'is_recruited': bool(account_data[6]), - 'recruited_count': recruited_count, - 'recruited_level_80_count': level_80_count - } - - # Información sobre la suspensión, si existe - if ban_data: - unban_timestamp, current_timestamp = ban_data - remaining_time = unban_timestamp - current_timestamp - user_info['is_banned'] = True - user_info['unban_date'] = datetime.fromtimestamp(unban_timestamp).strftime("%A %d de %B del %Y a las %H:%M:%S Horas") - user_info['remaining_time'] = remaining_time - else: - user_info['is_banned'] = False - - return user_info - - -def get_character_image(class_id, race, gender): - """ - Devuelve la imagen del personaje basada en la raza, clase y género. - `gender`: 0 = Hombre, 1 = Mujer - """ - # Definir los nombres de archivos según el género - gender_suffix = 'male' if gender == 0 else 'female' - - # Raza y clases compatibles - race_class_map = { - 10: [2, 3, 4, 5, 6, 8, 9], # Elfo de Sangre - 8: [1, 3, 4, 5, 6, 7, 8], # Trol - 6: [1, 3, 6, 7, 11], # Tauren - 5: [1, 4, 5, 6, 8, 9], # No-Muerto - 2: [1, 3, 4, 6, 7, 9], # Orco - 11: [1, 2, 3, 5, 6, 7, 8], # Draenei - 7: [1, 4, 6, 8, 9], # Gnomo - 4: [1, 3, 4, 5, 6, 11], # Elfo de la Noche - 3: [1, 2, 3, 4, 5, 6], # Enano - 1: [1, 2, 4, 5, 6, 8, 9] # Humano - } - - # Verificar si la clase es compatible con la raza - if class_id not in race_class_map.get(race, []): - return 'nw-themes/nw-ryu/nw-images/nw-classes/unknown.webp' # Imagen por defecto si no es compatible - - # Construir el nombre del archivo basado en la raza y el género - race_image_map = { - 10: f"big-blood-elf-{gender_suffix}.webp", - 8: f"big-troll-{gender_suffix}.webp", - 6: f"big-tauren-{gender_suffix}.webp", - 5: f"big-undead-{gender_suffix}.webp", - 2: f"big-orc-{gender_suffix}.webp", - 11: f"big-draenei-{gender_suffix}.webp", - 7: f"big-gnome-{gender_suffix}.webp", - 4: f"big-night-elf-{gender_suffix}.webp", - 3: f"big-dwarf-{gender_suffix}.webp", - 1: f"big-human-{gender_suffix}.webp" - } - - # Devolver la imagen según la raza y género, o una imagen por defecto - return f"nw-themes/nw-ryu/nw-images/nw-races/{race_image_map.get(race, 'unknown.webp')}" - -def get_account_characters(account_id): - """Obtiene los personajes de la cuenta dada.""" - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, level, money, race, class, zone, gender - FROM characters - WHERE account = %s - """, [account_id]) - characters = cursor.fetchall() - - character_list = [] - for char in characters: - name, level, money, race, class_id, zone, gender = char - gold = money // 10000 - silver = (money % 10000) // 100 - copper = money % 100 - - # Obtener la clase CSS y la imagen antes de pasarla a la plantilla - class_css = get_class_css(class_id) - image_url = get_character_image(class_id, race, gender) - - # Obtener el nombre de la zona usando la función get_zone_name - zone_name = get_zone_name(zone) - - character_list.append({ - 'name': name, - 'level': level, - 'gold': gold, - 'silver': silver, - 'copper': copper, - 'race': race, - 'class_id': class_id, - 'zone': zone_name, - 'class_css': class_css, - 'image_url': image_url - }) - return character_list - - -def change_password_view(request): - # Verificar si el usuario está autenticado mediante la sesión - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el usuario desde la base de datos de `acore_auth` - user_data = get_user_from_acore(username) - if not user_data: - return redirect('login') - - user_id = user_data['id'] - email = user_data['email'] - - if request.method == 'POST': - current_password = request.POST.get('cur-password', '').strip() - new_password = request.POST.get('new-password', '').strip() - conf_new_password = request.POST.get('conf-new-password', '').strip() - security_token = request.POST.get('security-token', '').strip() - - # Validar campos vacíos - if not current_password or not new_password or not conf_new_password or not security_token: - return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) - - # Validar que la nueva contraseña coincida con su confirmación - if new_password != conf_new_password: - return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'}) - - # Validar longitud de la nueva contraseña - if len(new_password) > 16: - return JsonResponse({'success': False, 'message': 'La contraseña nueva no debe exceder los 16 caracteres.'}) - - # Verificar si el token de seguridad ha sido generado - existing_token = SecurityToken.objects.filter(user_id=user_id).first() - if not existing_token: - return JsonResponse({'success': False, 'message': 'No tienes un token de seguridad generado. Por favor, genera uno antes de cambiar tu contraseña.'}) - - # Verificar si el token proporcionado es correcto - if existing_token.token != security_token: - return JsonResponse({'success': False, 'message': 'El token ingresado es incorrecto.'}) - - # Verificar la contraseña actual contra la cuenta Battle.net (por email) - if not authenticate(email, current_password): - return JsonResponse({'success': False, 'message': 'La contraseña actual es incorrecta.'}) - - # Generar salt y verifier SRP6 v2 para la nueva contraseña (bnet) - new_salt_bytes, new_verifier_bytes, srp_version = bnet.bnet_make_registration(email, new_password) - - # Localizar la cuenta bnet (por sesión o por email) y actualizarla - bnet_id = request.session.get('bnet_id') - with connections['acore_auth'].cursor() as cursor: - if not bnet_id: - account = bnet.get_bnet_account_by_email(cursor, email) - bnet_id = account['id'] if account else None - if not bnet_id: - return JsonResponse({'success': False, 'message': 'No se ha encontrado la cuenta.'}) - cursor.execute( - "UPDATE battlenet_accounts SET srp_version = %s, salt = %s, verifier = %s WHERE id = %s", - [srp_version, new_salt_bytes, new_verifier_bytes, bnet_id] - ) - - # Cerrar la sesión por seguridad - for key in ('username', 'account_id', 'bnet_id', 'bnet_email'): - request.session.pop(key, None) - - # Redirigir al usuario a la página de inicio de sesión - return JsonResponse({ - 'success': True, - 'message': 'Contraseña cambiada exitosamente. Has sido desconectado por seguridad.', - 'redirect': True - }) - - return render(request, 'account/change_password.html') - - -def get_user_from_acore(username): - """ - Obtiene la información del usuario desde la base de datos `acore_auth`. - """ - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, email FROM account WHERE username = %s", [username]) - result = cursor.fetchone() - if result: - return {'id': result[0], 'email': result[1]} - return None - -def security_token_view(request): - # Verificar si el usuario está autenticado mediante la sesión - username = request.session.get('username') - if not username: - return redirect('index') - - # Obtener el usuario desde la base de datos de `acore_auth` - user_data = get_user_from_acore(username) - if not user_data: - return redirect('index') - - user_id = user_data['id'] - email = user_data['email'] - ip_address = request.META.get('REMOTE_ADDR') - - # Verificar si existe un token activo para mostrar la fecha en la plantilla - existing_token = SecurityToken.objects.filter(user_id=user_id).first() - - # Obtener la fecha del token o "Sin solicitar" si no hay token - token_date = "Sin solicitar" - if existing_token: - token_date = existing_token.created_at.strftime('%H:%M:%S %d-%m-%Y') - - if request.method == 'POST': - # Verificar si el correo del usuario está vacío - if not email: - # Si no tiene correo, devolver un mensaje de error en formato JSON - return JsonResponse({ - 'success': False, - 'message': f'Debes añadir un correo electrónico a tu cuenta antes de solicitar un Token de seguridad.' - }) - # Verificar si ya existe un token y si se puede generar uno nuevo después de 7 días - if existing_token: - # Comprobar si han pasado al menos 7 días desde la creación del token - days_since_creation = (timezone.now() - existing_token.created_at).days - if days_since_creation < 7: - remaining_days = 7 - days_since_creation - return JsonResponse({ - 'success': False, - 'message': f'Sólo puedes solicitar un nuevo Token de seguridad cada 7 días, faltan {remaining_days} días para generar un nuevo Token.' - }) - - # Generar un nuevo token - token = secrets.token_urlsafe(4)[:6] - expires_at = timezone.now() + timedelta(days=7) - - # Eliminar el token anterior y crear uno nuevo - if existing_token: - existing_token.delete() - - # Crear y guardar el nuevo token - new_token = SecurityToken.objects.create( - user_id=user_id, - token=token, - ip_address=ip_address, - expires_at=expires_at # Esta fecha solo se usa para el control de los 7 días - ) - - # Enviar el correo con el token al usuario - send_security_token_email(email, username, token, ip_address) - - # Obtener la nueva fecha del token - new_token_date = new_token.created_at.strftime('%H:%M:%S %d-%m-%Y') - - # Responder con un mensaje exitoso y la nueva fecha - return JsonResponse({ - 'success': True, - 'message': f'Se ha enviado el Token de seguridad al correo {email}.', - 'token_date': new_token_date - }) - - # Pasar `token_date` al contexto de la plantilla - return render(request, 'account/security_token.html', { - 'token_date': token_date - }) - -def send_security_token_email(email, username, token, ip_address): - """ - Envía un correo electrónico con el token de seguridad. - """ - context = { - 'username': username, - 'token': token, - 'ip_address': ip_address, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - - # Llamada a tu función personalizada `enviar_correo` - enviar_correo( - subject=f'Token de seguridad de la cuenta {username} - {settings.NOMBRE_SERVIDOR}', - to_email=email, - template='emails/security_token.html', # Asegúrate de que este template exista - context=context - ) - -def change_email_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el usuario desde la base de datos de `acore_auth` - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, reg_mail - FROM account - WHERE username = %s - """, [username]) - account_data = cursor.fetchone() - - if not account_data: - return redirect('login') - - user_id, current_email, reg_email = account_data - security_token = SecurityToken.objects.filter(user_id=user_id).first() - - if request.method == 'POST': - print("Datos recibidos:", request.POST) - - current_password = request.POST.get('cur-password', '').strip() - current_email_input = request.POST.get('cur-email', '').strip() - new_email = request.POST.get('new-email', '').strip() - conf_new_email = request.POST.get('conf-new-email', '').strip() - token = request.POST.get('security-token', '').strip() - - # Validar los campos - if not all([current_password, current_email_input, new_email, conf_new_email, token]): - return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) - - if current_email_input != current_email: - return JsonResponse({'success': False, 'message': 'El correo actual no es correcto.'}) - - if new_email != conf_new_email: - return JsonResponse({'success': False, 'message': 'Los correos no coinciden.'}) - - if not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', new_email): - return JsonResponse({'success': False, 'message': 'El nuevo correo no es válido.'}) - - if not security_token or security_token.token != token: - return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) - - # Verificar la contraseña contra la cuenta Battle.net (por email actual) - if not authenticate(current_email, current_password): - return JsonResponse({'success': False, 'message': 'Contraseña incorrecta.'}) - - # Generar hashes - old_email_hash = secrets.token_urlsafe(16) - new_email_hash = secrets.token_urlsafe(16) - - # Guardar en `AccountActivation`. Se conserva la contraseña porque al - # cambiar el email hay que recalcular el verifier de bnet (el usuario SRP - # depende del email). - AccountActivation.objects.create( - username=username, - email=new_email, - old_email=current_email, - password=current_password, - recruiter_id=user_id, - hash=new_email_hash, - old_email_hash=old_email_hash - ) - - # Enviar confirmación al correo actual - confirm_old_email_link = f"{settings.URL_PRINCIPAL}/es/confirm-old-email?hash={old_email_hash}" - context_old_email = { - 'username': username, - 'confirm_link': confirm_old_email_link, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - enviar_correo( - subject=f'Confirme el cambio de correo para {username}', - to_email=current_email, - template='emails/confirm_old_email.html', - context=context_old_email - ) - - return JsonResponse({'success': True, 'message': 'Se ha enviado un correo de confirmación al correo actual.'}) - - return render(request, 'account/change_email.html') - - -def confirm_old_email_view(request): - hash = request.GET.get('hash') - activation = AccountActivation.objects.filter(old_email_hash=hash, is_used=False).first() - - if not activation or activation.is_expired(): - return JsonResponse({ - 'success': False, - 'message': 'Enlace no válido o expirado.', - 'redirect_url': reverse('expired-link') - }) - - # Marcar solo el old_email_hash como usado - activation.is_used = True - activation.save() - - # Enviar correo al nuevo correo para confirmar el cambio - activation_link = f"{settings.URL_PRINCIPAL}/es/confirm-new-email?hash={activation.hash}" - context_new_email = { - 'username': activation.username, - 'activation_link': activation_link, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - enviar_correo( - subject=f'Confirme su nuevo correo para {activation.username}', - to_email=activation.email, - template='emails/confirm_new_email.html', - context=context_new_email - ) - - return JsonResponse({'success': True, 'message': 'Correo confirmado. Se ha enviado un enlace al nuevo correo.'}) - -def confirm_new_email_view(request): - hash = request.GET.get('hash') - activation = AccountActivation.objects.filter(hash=hash, is_new_email_used=False).first() - - if not activation or activation.is_expired(): - return JsonResponse({ - 'success': False, - 'message': 'Enlace no válido o expirado.', - 'redirect_url': reverse('expired-link') - }) - - # Marcar solo el new_email_hash como usado - activation.is_new_email_used = True - activation.save() - - # Cambiar el email en la cuenta Battle.net implica recalcular salt/verifier, - # porque el usuario SRP = SHA256(UPPER(email)). - new_email_norm = bnet.normalize_email(activation.email) - old_email_norm = bnet.normalize_email(activation.old_email or '') - new_salt, new_verifier, srp_version = bnet.bnet_make_registration(new_email_norm, activation.password) - - with connections['acore_auth'].cursor() as cursor: - # Localizar la cuenta bnet por su email actual (el antiguo) - cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [old_email_norm]) - row = cursor.fetchone() - if not row: - return JsonResponse({ - 'success': False, - 'message': 'No se ha encontrado la cuenta Battle.net.', - }) - bnet_id = row[0] - - # Actualizar email + verifier en battlenet_accounts - cursor.execute( - "UPDATE battlenet_accounts SET email = %s, srp_version = %s, salt = %s, verifier = %s WHERE id = %s", - [new_email_norm, srp_version, new_salt, new_verifier, bnet_id] - ) - # Actualizar el email de las cuentas de juego enlazadas (cosmético) - cursor.execute( - "UPDATE account SET email = %s, reg_mail = %s WHERE battlenet_account = %s", - [new_email_norm, new_email_norm, bnet_id] - ) - - # Enviar notificación al correo anterior - context_old_email = { - 'username': activation.username, - 'new_email': activation.email, - 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR - } - enviar_correo( - subject=f'Su correo ha sido cambiado en {settings.NOMBRE_SERVIDOR}', - to_email=activation.old_email, - template='emails/old_email_notification.html', - context=context_old_email - ) - - return JsonResponse({'success': True, 'message': 'El cambio de correo ha sido confirmado y completado con éxito.'}) - -def expired_link_view(request): - return render(request, 'account/expired_link.html') - -def promo_code_view(request): - return render(request, 'account/promo_code.html') - -def rename_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # 🔹 Obtener información de la cuenta desde acore_auth - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # 🔹 Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - # 🔹 Obtener el precio del renombrado - price_obj = RenamePrice.objects.first() - price = price_obj.price if price_obj else 2.00 - - # 🔹 Ejecutar el comando SOAP para verificar el servidor - command = f".server info" - response = execute_soap_command(command) - - if request.method == 'POST': - character_name = request.POST.get('character') - - if response is None: - return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) - - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para renombrar este personaje.'}) - - # 🔹 Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('rename-success')) - cancel_url = request.build_absolute_uri(reverse('rename-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Renombrar personaje: {character_name}" - ) - - if stripe_response["success"]: - request.session['rename_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/rename_character.html', {'characters': characters, 'price': price}) - -@require_paid_stripe() -def rename_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('rename_character') - if not character_name: - return redirect('rename-character') - - command = f".char rename {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido marcado para renombrar." - request.session.pop('rename_character', None) # Limpiar la sesión - return render(request, 'account/rename_success.html', {'message': message}) - else: - return render(request, 'account/rename_success.html', {'message': f"Error: {response}"}) - -def rename_cancel_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - return render(request, 'account/rename_cancel.html') - -def restore_character_view(request): - # Verifica si el usuario está autenticado y obtiene su nombre de usuario - username = request.session.get('username') - if not username: - return redirect('login') # Redirige si no hay sesión - - # Obtener la ID de cuenta del usuario conectado a partir del sistema de autenticación personalizada - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [username]) - result = cursor.fetchone() - - if not result: - # Si no se encuentra la cuenta, mostrar un error - messages.error(request, "No se encontró la cuenta asociada a ese usuario.") - return redirect('login') # Redirige al login si no se encuentra la cuenta - - account_id = result[0] # Obtén el ID de la cuenta - except Exception as e: - messages.error(request, f"Ocurrió un error al recuperar la cuenta: {str(e)}") - return redirect('login') - - # Verificar si se ha seleccionado un personaje para restaurar - if request.method == 'POST': - # Obtener el GUID del personaje seleccionado para restaurar - guid = request.POST.get('guid') - - if not guid: - messages.error(request, "No se seleccionó un personaje para restaurar.") - return redirect('restore-character') - - # Aquí es donde usamos SOAP en lugar de la actualización directa - try: - command = f".char del restore {guid}" # Comando SOAP para restaurar el personaje - response = execute_soap_command(command) # Ejecuta el comando SOAP - - # Si la respuesta es exitosa, informamos al usuario - if response == "success": - messages.success(request, "Personaje restaurado exitosamente.") - return redirect('my-account') # Redirigir a la cuenta del usuario - else: - messages.error(request, "Hubo un error al restaurar el personaje.") - return redirect('restore-character') - - except Exception as e: - messages.error(request, f"Hubo un error al intentar restaurar el personaje: {str(e)}") - return redirect('restore-character') - - # Realizar la consulta para obtener los personajes eliminados de la cuenta - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT guid, level, deleteInfos_Name, deleteInfos_Account, deleteDate - FROM characters - WHERE deleteInfos_Account = %s AND deleteDate IS NOT NULL - """, [account_id]) # Filtra por account_id - - # Obtener los resultados de la consulta - deleted_characters = cursor.fetchall() - - # Si no hay personajes eliminados - if not deleted_characters: - return render(request, 'account/restore_character.html', { - 'message': 'No hay personajes eliminados que puedas recuperar.' - }) - - # Crear un listado de personajes eliminados con su nombre y nivel (si lo tienes) - character_list = [] - for character in deleted_characters: - guid, level, name, account_id, delete_date = character - # Agregar la información del personaje al listado - character_list.append({ - 'name': name, - 'guid': guid, - 'level': level, - 'delete_date': delete_date - }) - - # Renderiza la plantilla con los personajes eliminados - return render(request, 'account/restore_character.html', { - 'character_list': character_list - }) - -def restore_items_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - characters = [] - deleted_items = [] - selected_character = None - - try: - # Obtener la ID de cuenta del usuario - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [username]) - result = cursor.fetchone() - if result: - account_id = result[0] - else: - messages.error(request, "No se encontró la cuenta asociada.") - return redirect('login') - - # Obtener los personajes del usuario - with connections['acore_characters'].cursor() as cursor: - cursor.execute("SELECT name FROM characters WHERE account = %s", [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - except Exception as e: - messages.error(request, f"Error al obtener los personajes: {str(e)}") - - if request.method == "GET" and "character_name" in request.GET: - selected_character = request.GET.get("character_name") - - if selected_character: - try: - # Ejecutar comando SOAP para obtener ítems eliminados del personaje seleccionado - command = f".item restore list {selected_character}" - response = execute_soap_command(command) - - # Procesar la respuesta del servidor - for line in response.split("\n"): - match = re.search(r"Recover id: (\d+) \| Item: (.+) \((\d+)\) \| Count: (\d+)", line) - if match: - recover_id = match.group(1) - item_name = match.group(2) - item_id = match.group(3) - count = match.group(4) - deleted_items.append({ - "recover_id": recover_id, - "item_name": item_name, - "item_id": item_id, - "count": count - }) - - except Exception as e: - messages.error(request, f"Error al obtener los ítems eliminados: {str(e)}") - - if request.method == "POST": - recover_id = request.POST.get("recover_id") - selected_character = request.POST.get("character_name") - - if not recover_id or not selected_character: - messages.error(request, "Selecciona un personaje y un ítem para restaurar.") - return redirect('restore-items') - - try: - # Ejecutar comando SOAP para restaurar el ítem - command = f".item restore {recover_id} {selected_character}" - response = execute_soap_command(command) - - if "success" in response.lower(): - messages.success(request, "Ítem restaurado exitosamente.") - else: - messages.error(request, "No se pudo restaurar el ítem.") - except Exception as e: - messages.error(request, f"Error al restaurar el ítem: {str(e)}") - - return redirect('restore-items') - - return render(request, 'account/restore_items.html', { - 'characters': characters, - 'deleted_items': deleted_items, - 'selected_character': selected_character - }) - - -def quest_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - return render(request, 'account/quest_character.html') - -def send_gift_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - return render(request, 'account/send_gift.html') - -def customize_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # ?? Obtener información de la cuenta desde acore_auth - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # ?? Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - - characters = [row[0] for row in cursor.fetchall()] - - # Obtener el precio de la personalización (si se gestiona desde el admin) - price_obj = CustomizePrice.objects.first() - price = price_obj.price if price_obj else 1.00 - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para personalizar este personaje.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('customize-success')) - cancel_url = request.build_absolute_uri(reverse('customize-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Personalizar personaje: {character_name}" - ) - - - if stripe_response["success"]: - # Guardar temporalmente el nombre del personaje en la sesión - request.session['customize_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/customize_character.html', {'characters': characters, 'price': price}) - - -@require_paid_stripe() -def customize_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('customize_character') - if not character_name: - return redirect('customize-character') - - command = f".char customi {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido marcado para personalización." - request.session.pop('customize_character', None) # Limpiar la sesión - return render(request, 'account/customize_success.html', {'message': message}) - else: - return render(request, 'account/customize_success.html', {'message': f"Error: {response}"}) - - -def customize_cancel_view(request): - return render(request, 'account/customize_cancel.html') - - -def change_race_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # ?? Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - # Obtener el precio del cambio de raza - price_obj = ChangeRacePrice.objects.first() - price = price_obj.price if price_obj else 10.00 # Precio predeterminado si no hay registro - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para cambiar la raza de este personaje.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('change-race-success')) - cancel_url = request.build_absolute_uri(reverse('change-race-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Cambio de Raza del personaje: {character_name}" - ) - - if stripe_response["success"]: - # Guardar el personaje en la sesión para el cambio - request.session['change_race_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/change_race.html', {'characters': characters, 'price': price}) - - -@require_paid_stripe() -def change_race_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('change_race_character') - if not character_name: - return redirect('change-race') - - command = f".char changerace {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido marcado para cambio de raza." - request.session.pop('change_race_character', None) # Limpiar la sesión - return render(request, 'account/change_race_success.html', {'message': message}) - else: - return render(request, 'account/change_race_success.html', {'message': f"Error: {response}"}) - - -def change_race_cancel_view(request): - return render(request, 'account/change_race_cancel.html') - - -def change_faction_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # ?? Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - # Obtener el precio del cambio de facción - price_obj = ChangeFactionPrice.objects.first() - price = price_obj.price if price_obj else 10.00 - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para cambiar la facción de este personaje.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('change-faction-success')) - cancel_url = request.build_absolute_uri(reverse('change-faction-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Cambiar Faccion del personaje: {character_name}" - ) - - if stripe_response["success"]: - # Guardar el personaje seleccionado en la sesión - request.session['change_faction_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/change_faction.html', {'characters': characters, 'price': price}) - - -@require_paid_stripe() -def change_faction_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('change_faction_character') - if not character_name: - return redirect('change-faction-character') - - command = f".char changef {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha cambiado de facción con éxito." - request.session.pop('change_faction_character', None) # Limpiar la sesión - return render(request, 'account/change_faction_success.html', {'message': message}) - else: - return render(request, 'account/change_faction_success.html', {'message': f"Error: {response}"}) - - -def change_faction_cancel_view(request): - return render(request, 'account/change_faction_cancel.html') - - - -def level_up_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # ?? Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name FROM characters WHERE account = %s - """, [account_id]) - characters = [row[0] for row in cursor.fetchall()] - - # Obtener el precio de subir a nivel 80 - price_obj = LevelUpPrice.objects.first() - price = price_obj.price if price_obj else 10.00 - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para subir de nivel a este personaje.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('level-up-success')) - cancel_url = request.build_absolute_uri(reverse('level-up-cancel')) - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=price, - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Subir el Nivel 80 del personaje: {character_name}" - ) - - if stripe_response["success"]: - # Guardar el personaje en la sesión para usarlo después del pago - request.session['levelup_character'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/level_up.html', {'characters': characters, 'price': price}) - - -@require_paid_stripe() -def level_up_success_view(request): - character_name = request.session.get('levelup_character') - if not character_name: - return redirect('level-up-character') - - # Ejecutar el comando SOAP - command = f".char level {character_name} 80" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido subido al nivel 80." - else: - message = f"Error: {response}" - - # Limpiar la sesión - request.session.pop('levelup_character', None) - - return render(request, 'account/level_up_success.html', {'message': message}) - - -def level_up_cancel_view(request): - return render(request, 'account/level_up_cancel.html') - - -def gold_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes y estado de conexión del usuario - try: - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, online, money FROM characters WHERE account = %s - """, [account_id]) - characters_data = cursor.fetchall() - - characters = {row[0]: {'online': row[1], 'money': row[2]} for row in characters_data} - - except Exception as e: - return JsonResponse({'success': False, 'message': f'Error al obtener los personajes: {str(e)}'}) - - # Obtener precios del oro - gold_prices = GoldPrice.objects.all().order_by('gold_amount') - - if request.method == 'POST': - character_name = request.POST.get('character') - gold_amount = request.POST.get('gold_amount') - - # Validaciones iniciales - if not character_name or not gold_amount: - return JsonResponse({'success': False, 'message': 'Seleccione un personaje y una cantidad de oro.'}) - - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para enviar oro a este personaje.'}) - - if characters[character_name]['online'] == 1: - return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para recibir oro.'}) - - try: - gold_amount = int(gold_amount) - except ValueError: - return JsonResponse({'success': False, 'message': 'Cantidad de oro no válida.'}) - - gold_price = GoldPrice.objects.filter(gold_amount=gold_amount).first() - if not gold_price: - return JsonResponse({'success': False, 'message': 'Cantidad de oro no válida.'}) - - # El precio va en euros (con decimales); el helper lo pasa a céntimos (×100) - stripe_price = gold_price.price - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('gold-success')) - cancel_url = request.build_absolute_uri(reverse('gold-cancel')) - - # Llamada corregida a create_checkout_session con todos los parámetros requeridos - stripe_response = create_checkout_session( - account_id=account_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=stripe_price, # El precio calculado - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Aumentar el Oro: {gold_amount} al Personaje: {character_name}" - ) - - if stripe_response["success"]: - # Guardar en sesión los datos para el callback tras el pago - request.session['gold_transaction'] = { - 'character_name': character_name, - 'gold_amount': gold_amount - } - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/gold_character.html', {'characters': characters, 'gold_prices': gold_prices}) - - -@require_paid_stripe() -def gold_success_view(request): - # Procesar el pago exitoso - transaction = request.session.pop('gold_transaction', None) - if not transaction: - return redirect('gold-character') - - character_name = transaction['character_name'] - gold_amount = int(transaction['gold_amount']) - - # Calcular el oro en formato del juego - game_gold_amount = gold_amount * 10000 - - # Actualizar la cantidad de oro en la base de datos - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - UPDATE characters - SET money = money + %s - WHERE name = %s - """, [game_gold_amount, character_name]) - - return render(request, 'account/gold_success.html', {'character_name': character_name, 'gold_amount': gold_amount}) - - -def gold_cancel_view(request): - return render(request, 'account/gold_cancel.html') - - -def transfer_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el precio de transferencia - price_obj = TransferPrice.objects.first() - price = price_obj.price if price_obj else 10.00 - - # Obtener el ID de usuario desde acore_auth.account - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, email, last_ip FROM account WHERE username = %s", [username]) - user_data = cursor.fetchone() - if not user_data: - return JsonResponse({'success': False, 'message': 'Cuenta no encontrada. Por favor, inicie sesión nuevamente.'}) - - user_id, email, acore_ip = user_data # ID de la cuenta, email y la última IP - - # Obtener personajes de la cuenta de origen - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, class, online - FROM characters - WHERE account = %s - """, [user_id]) - characters = cursor.fetchall() - - if request.method == 'POST': - character_name = request.POST.get('character') - destination_account = request.POST.get('destination_account') - token = request.POST.get('security_token') - - # Validaciones - if not all([character_name, destination_account, token]): - return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) - - character = next((char for char in characters if char[0] == character_name), None) - if not character: - return JsonResponse({'success': False, 'message': 'No tienes permiso para transferir este personaje.'}) - - # Verificar si el personaje está desconectado - if character[2] == 1: # Si está conectado - return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser transferido.'}) - - # Verificar el token de seguridad - security_token = SecurityToken.objects.filter(user_id=user_id, token=token).first() - if not security_token: - return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) - - # Verificar si el token ha expirado - if security_token.expires_at < timezone.now(): - return JsonResponse({'success': False, 'message': 'El token ha expirado. Solicite uno nuevo.'}) - - # Verificar si la cuenta de destino existe - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [destination_account]) - destination_account_data = cursor.fetchone() - - if not destination_account_data: - return JsonResponse({'success': False, 'message': 'La cuenta de destino no existe.'}) - - # Verificar restricciones en la cuenta de destino - destination_account_id = destination_account_data[0] - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT account, name, class, level, online - FROM characters - WHERE account = %s - """, [destination_account_id]) - destination_characters = cursor.fetchall() - - if not any(char[3] >= 55 and char[2] != 6 for char in destination_characters): - return JsonResponse({'success': False, 'message': 'La cuenta destino debe tener al menos un personaje de nivel 55 o superior que no sea Caballero de la Muerte.'}) - - if any(char[2] == 6 for char in destination_characters): - return JsonResponse({'success': False, 'message': 'La cuenta destino no puede tener un Caballero de la Muerte.'}) - - # Verificar que ninguna cuenta esté online - for char in destination_characters: - if char[4] == 1: - return JsonResponse({'success': False, 'message': 'Todos los personajes de la cuenta destino deben estar desconectados.'}) - for char in characters: - if char[2] == 1: - return JsonResponse({'success': False, 'message': 'Todos los personajes de la cuenta origen deben estar desconectados.'}) - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('transfer-success')) - cancel_url = request.build_absolute_uri(reverse('transfer-cancel')) - - # Llamada corregida a create_checkout_session con todos los parámetros requeridos - stripe_response = create_checkout_session( - username=username, - email=email, - acore_ip=acore_ip, - account_id=user_id, # Pasar el account_id - amount=price, # El precio de la transferencia - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Transferir el personaje: {character_name} a la cuenta de destino: {destination_account}" - ) - - if stripe_response["success"]: - # Guardar los datos de la transferencia en la sesión para usarlos después del pago - request.session['transfer_data'] = { - 'character_name': character_name, - 'destination_account': destination_account - } - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/transfer_character.html', {'characters': characters, 'price': price}) - - - - -@require_paid_stripe() -def transfer_success_view(request): - transfer_data = request.session.get('transfer_data') - if not transfer_data: - return redirect('transfer-character') - - character_name = transfer_data['character_name'] - destination_account = transfer_data['destination_account'] - - # Ejecutar comando SOAP - command = f".char changeaccount {destination_account} {character_name}" - response = execute_soap_command(command) - - if response is None or response.strip() == "": - message = f"El personaje {character_name} ha sido transferido a la cuenta {destination_account}." - request.session.pop('transfer_data', None) # Limpiar sesión - return render(request, 'account/transfer_success.html', {'message': message}) - else: - return render(request, 'account/transfer_success.html', {'message': f"Error: {response}"}) - - -def transfer_cancel_view(request): - return render(request, 'account/transfer_cancel.html') - -def store_novawow_success_view(request): - return render(request, 'store/store_novawow_success.html') - -def store_novawow_cancel_view(request): - return render(request, 'store/store_novawow_cancel.html') - - -def store_novawow_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - category_name = request.GET.get("category") - - # Obtener personajes del usuario desde la base de datos acore_characters - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name - FROM characters - WHERE account = ( - SELECT id - FROM acore_auth.account - WHERE username = %s - ) - """, [username]) - characters = [row[0] for row in cursor.fetchall()] - - # Obtener categorías e ítems - categories = Category.objects.prefetch_related("items").all() - store_data = {category.name: list(category.items.values()) for category in categories} - - # Si se está solicitando una categoría específica, devolver solo los ítems de esa categoría - if category_name: - try: - category = Category.objects.get(name=category_name) - except Category.DoesNotExist: - return JsonResponse({'success': False, 'message': 'Categoría no encontrada'}, status=404) - return JsonResponse({'success': True, 'items': list(category.items.values())}) - - # Obtener carrito desde la sesión - carrito = request.session.get('carrito', []) - - # Calcular el precio total con 2 decimales - total_price = sum(Decimal(item['precio']) for item in carrito) - total_price_with_iva = (total_price * Decimal('1.21')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) - - if request.method == 'POST': - # Obtener datos del body - data = json.loads(request.body) - character_name = data.get('character') - carrito_cliente = data.get('carrito') or [] - - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, selecciona un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para este personaje.'}) - - if not carrito_cliente: - return JsonResponse({'success': False, 'message': 'El carrito está vacío.'}) - - # VALIDAR ÍTEMS Y PRECIOS CONTRA LA BASE DE DATOS (nunca confiar en el - # precio ni en los ítems que manda el cliente). - try: - ids_solicitados = [int(item['id']) for item in carrito_cliente] - except (KeyError, TypeError, ValueError): - return JsonResponse({'success': False, 'message': 'Carrito no válido.'}) - - items_db = {i.item_id: i for i in Item.objects.filter(item_id__in=ids_solicitados)} - - carrito = [] # carrito saneado que se guardará en sesión - total_price = Decimal('0.00') - for item in carrito_cliente: - item_id = int(item['id']) - db_item = items_db.get(item_id) - if not db_item: - return JsonResponse({'success': False, 'message': f'Ítem no disponible en la tienda (ID {item_id}).'}) - try: - cantidad = max(1, int(item.get('cantidad', 1))) - except (TypeError, ValueError): - cantidad = 1 - total_price += db_item.price * cantidad - carrito.append({'id': item_id, 'nombre': db_item.name, 'cantidad': cantidad}) - - total_price_with_iva = (total_price * Decimal('1.21')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) - - # Guardar en sesión el carrito YA VALIDADO (para la entrega tras el pago) - request.session['carrito'] = carrito - request.session.modified = True - - # Descripción del producto a partir de los datos validados - product_description = ", ".join([f"{it['nombre']} x{it['cantidad']}" for it in carrito]) - - # Obtener detalles del usuario - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id, email, last_ip FROM account WHERE username = %s", [username]) - user_data = cursor.fetchone() - - if not user_data: - return JsonResponse({'success': False, 'message': 'Cuenta no encontrada.'}) - - user_id, email, acore_ip = user_data # ID de la cuenta, email y la última IP - - # Crear sesión de pago con Stripe - success_url = request.build_absolute_uri(reverse('store-novawow/success')) - cancel_url = request.build_absolute_uri(reverse('store-novawow/cancel')) - - # Llamada corregida a create_checkout_session con todos los parámetros requeridos - stripe_response = create_checkout_session( - username=username, - email=email, - acore_ip=acore_ip, - account_id=user_id, # Pasar el account_id - amount=total_price_with_iva, # El precio con IVA - character_name=character_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Compra de ítems para el personaje: {character_name} : {product_description}" - ) - - if stripe_response["success"]: - request.session['store_novawow'] = character_name - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'store/store_novawow.html', { - 'characters': characters, - 'store_data': store_data, - 'categories': categories, - 'total_price': total_price_with_iva - }) - - -from collections import Counter - -@require_paid_stripe(redirect_to='store-novawow') -def store_novawow_success_view(request): - # Procesar el comando SOAP tras el pago exitoso - character_name = request.session.get('store_novawow') - carrito = request.session.get('carrito', []) - - if not character_name or not carrito: - return redirect('store-novawow') - - # Contar correctamente los ítems por ID - item_counts = Counter(item["id"] for item in carrito) - - # Generar la lista de ítems en formato "ID:Cantidad" - items_str = " ".join(f"{item_id}:{count}" for item_id, count in item_counts.items()) - - # Construir el comando SOAP - command = f".send items {character_name} 'Gracias por donar a Nova WoW' 'Aquí tienes tu donación' {items_str}" - - # Ejecutar el comando SOAP - response = execute_soap_command(command) - - # Limpiar la sesión después del envío - request.session.pop('store_novawow', None) - request.session.pop('carrito', None) - - if response is None or response.strip() == "": - message = f"Los ítems han sido añadidos al personaje {character_name} correctamente." - return render(request, 'store/store_novawow_success.html', {'message': message}) - else: - return render(request, 'store/store_novawow_success.html', {'message': f"Error: {response}"}) - - -def store_novawow_buscar_categoria_view(request, categoria): - try: - # Buscar la categoría desde el parámetro de la URL - category = Category.objects.get(name=categoria) - items = category.items.all().values("id", "name", "price", "image_url") - return JsonResponse({"success": True, "items": list(items)}) - except Category.DoesNotExist: - return JsonResponse({"success": False, "message": "Categoría no encontrada"}, status=404) - - -def revive_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, online - FROM characters - WHERE account = ( - SELECT id - FROM acore_auth.account - WHERE username = %s - ) - """, [username]) - characters_data = cursor.fetchall() - characters = {row[0]: row[1] for row in characters_data} # Diccionario con nombre y estado de conexión - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para revivir este personaje.'}) - - # Verificar si el personaje está desconectado - if characters[character_name] == 1: # Si está conectado - return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser revivido.'}) - - # Verificar el historial de uso del comando `revive` - last_revive = ReviveHistory.objects.filter(character_name=character_name).order_by('-used_at').first() - if last_revive and last_revive.used_at > now() - timedelta(hours=12): - return JsonResponse({'success': False, 'message': 'El personaje solo puede revivirse una vez cada 12 horas.'}) - - # Ejecutar el comando SOAP - command = f".revive {character_name}" - response = execute_soap_command(command) - - # None = fallo de comunicación con el servidor - if response is None: - return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) - - # Respuesta vacía = éxito (el comando .revive no devuelve texto si va bien) - if response.strip() == "": - ReviveHistory.objects.create(character_name=character_name, used_at=now()) - return JsonResponse({'success': True, 'message': f'El personaje {character_name} ha sido revivido con éxito.'}) - - # Respuesta no vacía = el servidor devolvió un mensaje/error - return JsonResponse({'success': False, 'message': f'{response}'}) - - return render(request, 'account/revive_character.html', {'characters': characters.keys()}) - - -def unstuck_character_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener personajes del usuario desde la base de datos `acore_characters` - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT name, online - FROM characters - WHERE account = ( - SELECT id - FROM acore_auth.account - WHERE username = %s - ) - """, [username]) - characters_data = cursor.fetchall() - characters = {row[0]: row[1] for row in characters_data} # Diccionario con nombre y estado de conexión - - if request.method == 'POST': - character_name = request.POST.get('character') - - # Validar si el personaje fue seleccionado - if not character_name: - return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) - - # Verificar si el personaje pertenece al usuario - if character_name not in characters: - return JsonResponse({'success': False, 'message': 'No tienes permiso para desbloquear este personaje.'}) - - # Verificar si el personaje está desconectado - if characters[character_name] == 1: # Si está conectado - return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser desbloqueado.'}) - - # Verificar el historial de uso del comando - last_unstuck = UnstuckHistory.objects.filter(character_name=character_name).order_by('-used_at').first() - if last_unstuck and last_unstuck.used_at > now() - timedelta(hours=12): - return JsonResponse({'success': False, 'message': 'El personaje solo puede desbloquearse una vez cada 12 horas.'}) - - # Ejecutar el comando SOAP - command = f".unstuck {character_name} startzone" - response = execute_soap_command(command) - - if response is None: - return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) - - # Analizar la respuesta - if f"You are summoning {character_name} (offline)" in response: - # Registrar el uso del comando - UnstuckHistory.objects.create(character_name=character_name, used_at=now()) - return JsonResponse({'success': True, 'message': f'El personaje {character_name} ha sido desbloqueado con éxito.'}) - else: - return JsonResponse({'success': False, 'message': f'Error: {response}'}) - - return render(request, 'account/unstuck_character.html', {'characters': characters.keys()}) - -def transfer_d_points_view(request): - # Renderizar la plantilla para la transferencia de puntos - return render(request, 'account/transfer_d_points.html') - - -def rename_guild_view(request): - username = request.session.get('username') - if not username: - return redirect('login') - - user_data = get_user_from_acore(username) - if not user_data: - return redirect('login') - - user_id = user_data['id'] - email = user_data['email'] - - security_token = SecurityToken.objects.filter(user_id=user_id).first() - guild_settings, created = GuildRenameSettings.objects.get_or_create(defaults={'cost': 10}) - rename_cost = guild_settings.cost - - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT g.guildid, g.name, gm.rank, c.name - FROM guild g - JOIN guild_member gm ON g.guildid = gm.guildid - JOIN characters c ON c.guid = gm.guid - WHERE c.account = %s AND gm.rank = 0 - """, [user_id]) - guild_leader_data = cursor.fetchall() - - if not guild_leader_data: - return JsonResponse({'success': False, 'message': 'Error: No tienes personajes que sean Maestros de Hermandad en tu cuenta.'}) - - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id, email, last_ip FROM account WHERE username = %s - """, [username]) - account_info = cursor.fetchone() - - if not account_info: - return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) - - account_id, email, acore_ip = account_info - client_ip = request.META.get('REMOTE_ADDR', '') - - if request.method == 'POST': - old_guild_name = request.POST.get('old-guild-name', '').strip() - new_guild_name = request.POST.get('new-guild-name', '').strip() - conf_new_guild_name = request.POST.get('conf-new-guild-name', '').strip() - current_password = request.POST.get('cur-password', '').strip() - token = request.POST.get('security-token', '').strip() - - if not old_guild_name or not new_guild_name or not conf_new_guild_name or not current_password or not token: - return JsonResponse({'success': False, 'message': 'Por favor, completa todos los campos.'}) - - if not authenticate(email, current_password): - return JsonResponse({'success': False, 'message': 'La contraseña actual es incorrecta.'}) - - if not security_token or security_token.token != token: - return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) - - if new_guild_name != conf_new_guild_name: - return JsonResponse({'success': False, 'message': 'Los nombres nuevos no coinciden.'}) - - success_url = request.build_absolute_uri(reverse('guild-success')) - cancel_url = request.build_absolute_uri(reverse('guild-cancel')) - - stripe_response = create_checkout_session( - account_id=user_id, - username=username, - email=email, - acore_ip=acore_ip, - amount=rename_cost, - character_name=old_guild_name, - success_url=success_url, - cancel_url=cancel_url, - product_name=f"Renombrar hermandad: {old_guild_name} → {new_guild_name}" - ) - - if stripe_response["success"]: - request.session['guild_rename'] = { - 'old_name': old_guild_name, - 'new_name': new_guild_name - } - return JsonResponse({ - 'success': True, - 'session_id': stripe_response["session_id"], - 'stripe_public_key': settings.STRIPE_PUBLIC_KEY - }) - else: - return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) - - return render(request, 'account/rename_guild.html', { - 'guild_leader_data': guild_leader_data, - 'rename_cost': rename_cost - }) - - -@require_paid_stripe() -def guild_rename_success_view(request): - # Obtener los datos de la sesión - guild_rename_data = request.session.get('guild_rename') - if not guild_rename_data: - return redirect('rename-cancel') # Redirigir si no hay datos de renombrado - - old_guild_name = guild_rename_data.get('old_name') - new_guild_name = guild_rename_data.get('new_name') - - if not old_guild_name or not new_guild_name: - return redirect('rename-cancel') # Redirigir si faltan datos - - # Ejecutar el comando SOAP - command = f".guild rename {old_guild_name} {new_guild_name}" - response = execute_soap_command(command) - - # Limpiar la sesión después de la ejecución - request.session.pop('guild_rename', None) - - if response is None or response.strip() == "": - message = f"La Hermandad '{old_guild_name}' ha cambiado su nombre a '{new_guild_name}'. Cierra sesión y vuelve a entrar para que los cambios se reflejen en el juego." - return render(request, 'account/guild_rename_success.html', {'message': message}) - else: - return render(request, 'account/guild_rename_success.html', {'message': f"Error: {response}"}) - -def vote_points_view(request): - # Verificar si el usuario tiene un `username` en la sesión - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el `account_id` basado en el `username` - with connections['acore_auth'].cursor() as cursor: - cursor.execute("SELECT id FROM account WHERE username = %s", [username]) - account_data = cursor.fetchone() - - if not account_data: - return JsonResponse({'success': False, 'message': 'Cuenta no encontrada.'}) - - account_id = account_data[0] - - if request.method == 'POST': - vote_url = request.POST.get('vote').strip() - - site = VoteSite.objects.filter(url=vote_url).first() - if not site: - return JsonResponse({'success': False, 'message': 'Sitio de votación no encontrado.'}) - - # Verificar si ya ha votado en las últimas 12 horas y 30 minutos - last_vote = VoteLog.objects.filter(account_id=account_id, vote_site=site).order_by('-created_at').first() - now = timezone.now() - if last_vote: - time_diff = now - last_vote.created_at - cooldown_period = timedelta(hours=12, minutes=30) - - if time_diff < cooldown_period: - remaining_time = cooldown_period - time_diff - hours, remainder = divmod(remaining_time.seconds, 3600) - minutes, _ = divmod(remainder, 60) - return JsonResponse({ - 'success': False, - 'message': f'Tienes que esperar {hours} horas y {minutes} minutos antes de volver a votar.' - }) - - # Acreditar puntos al usuario y registrar el voto - user_points = HomeApiPoints.objects.filter(accountID=account_id).first() - if user_points: - user_points.vp += site.points - user_points.save() - else: - HomeApiPoints.objects.create(accountID=account_id, vp=site.points, dp=0) - - # Registrar el voto - VoteLog.objects.create(account_id=account_id, vote_site=site, last_vote_time=now) - - return JsonResponse({'success': True, 'message': f'Has votado en {site.name}. Se han acreditado {site.points} PV.'}) - - vote_sites = VoteSite.objects.all() - return render(request, 'account/vote_points.html', {'vote_sites': vote_sites}) - -def d_points_view(request): - """ - Vista para la página de D-Points. - Simplemente renderiza la plantilla d_points.html. - """ - return render(request, 'account/d_points.html') - - -def points_history_view(request): - return render(request, 'account/points_history.html') - - - -def format_datetime(timestamp): - if timestamp: - return datetime.utcfromtimestamp(timestamp).strftime('%d-%m-%Y %H:%M:%S') - return "No disponible" - - -def trans_history_view(request): - """ - Vista para mostrar el historial de transacciones de la cuenta y sus personajes. - """ - username = request.session.get('username') # Obtener el nombre de usuario de la sesión - - if username: - # Filtramos las transacciones que corresponden al usuario y a los personajes asociados - transactions = StripeLog.objects.filter(username=username) - else: - transactions = [] - - # Renderizamos la vista con el historial de transacciones - return render(request, 'account/trans_history.html', {'transactions': transactions}) - - -def get_account_id(username): - # Consulta para obtener el account_id a partir del username - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT id - FROM account - WHERE username = %s - """, [username]) - result = cursor.fetchone() - return result[0] if result else None - - -def get_ban_history(account_id): - # Consulta de baneos de la cuenta - with connections['acore_auth'].cursor() as cursor: - cursor.execute(""" - SELECT bandate, unbandate, banreason, active, account.username - FROM account_banned - JOIN account ON account_banned.id = account.id - WHERE account_banned.id = %s - """, [account_id]) - account_bans = cursor.fetchall() - - # Formatear las fechas - account_bans = [(format_datetime(ban[0]), format_datetime(ban[1]), ban[2], ban[3], ban[4]) for ban in account_bans] - - # Consulta de baneos de los personajes asociados a la cuenta - with connections['acore_characters'].cursor() as cursor: - cursor.execute(""" - SELECT bandate, unbandate, banreason, active, characters.name - FROM character_banned - JOIN characters ON character_banned.guid = characters.guid - WHERE characters.account = %s - """, [account_id]) - character_bans = cursor.fetchall() - - # Formatear las fechas - character_bans = [(format_datetime(ban[0]), format_datetime(ban[1]), ban[2], ban[3], ban[4]) for ban in character_bans] - - return account_bans, character_bans - - -def calculate_unmute_date(mute_date, mute_duration): - if isinstance(mute_date, int): - mute_date = datetime.fromtimestamp(mute_date) - return mute_date + timedelta(seconds=mute_duration) - - - -def ban_history_view(request): - # Verificar si el usuario tiene un `username` en la sesión - username = request.session.get('username') - if not username: - return redirect('login') - - # Obtener el account_id a partir del username - account_id = get_account_id(username) - if not account_id: - return redirect('login') # O redirigir a otra página si no se encuentra el account_id - - print(f"Account ID: {account_id}") # Depurar el account_id - - account_bans, character_bans = get_ban_history(account_id) - - return render(request, 'account/ban_history.html', { - 'account_bans': account_bans, - 'character_bans': character_bans, - 'now': datetime.now(), - }) - - -def security_history_view(request): - username = request.session.get('username') - if not username: - return redirect('login') # Si no está autenticado, redirigir al login - - # Obtener los últimos 20 intentos de inicio de sesión - login_attempts = LoginAttempt.objects.filter(username=username).order_by('-timestamp')[:20] - - return render(request, 'account/security_history.html', {'login_attempts': login_attempts}) - -def log_login_attempt(username, status, ip_address): - # Registrar intento de inicio de sesión en el historial - LoginAttempt.objects.create( - username=username, - status=status, - ip_address=ip_address, - timestamp=timezone.now() - ) - - -def trade_points_view(request): - """ - Vista para la página de intercambio de puntos. - """ - return render(request, 'account/trade_points.html') - -def guild_rename_cancel_view(request): - """ - Vista para la página de intercambio de puntos. - """ - return render(request, 'account/guild_rename_cancel.html') - - - -def help_view(request): - # Verificar si el usuario está logueado - if 'username' not in request.session: - # Si no está logueado, redirigir al inicio de sesión - return redirect('index') - - # Si está logueado, renderizar la página de ayuda - return render(request, 'community/help.html') - -def get_race_icon(race): - race_icons = { - 1: "human-male", - 2: "orc-male", - 3: "dwarf-male", - 4: "night-elf-male", - 5: "undead-male", - 6: "tauren-male", - 7: "gnome-male", - 8: "troll-male", - 9: "goblin-male", - 10: "blood-elf-male", - 11: "draenei-male" - } - return race_icons.get(race, "unknown") - - -def get_race_iconss(race): - race_iconss = { - 1: "human", - 2: "orc", - 3: "dwarf", - 4: "night-elf", - 5: "undead", - 6: "tauren", - 7: "gnome", - 8: "troll", - 9: "goblin", - 10: "blood-elf", - 11: "draenei" - } - return race_iconss.get(race, "unknown") - -def get_class_icon(char_class): - class_icons = { - 1: "warrior", - 2: "paladin", - 3: "hunter", - 4: "rogue", - 5: "priest", - 6: "death-knight", - 7: "shaman", - 8: "mage", - 9: "warlock", - 11: "druid" - } - return class_icons.get(char_class, "unknown") - - -def get_class_name(char_class): - names = { - 1: "Guerrero", - 2: "Paladín", - 3: "Cazador", - 4: "Pícaro", - 5: "Sacerdote", - 6: "Caballero de la Muerte", - 7: "Chamán", - 8: "Mago", - 9: "Brujo", - 11: "Druida" - } - return names.get(char_class, "Desconocido") - -def get_achievement_name(achievement_id): - achievement_names = { - 457: "¡Primero del reino! Nivel 80", - 458: "¡Primero del reino! Pícaro nivel 80", - 463: "¡Primero del reino! Brujo nivel 80", - 461: "¡Primero del reino! Caballero de la Muerte nivel 80", - 462: "¡Primero del reino! Cazador nivel 80", - 467: "¡Primero del reino! Chamán nivel 80", - 466: "¡Primero del reino! Druida nivel 80", - 459: "¡Primero del reino! Guerrero nivel 80", - 460: "¡Primero del reino! Mago nivel 80", - 465: "¡Primero del reino! Paladín nivel 80", - 464: "¡Primero del reino! Sacerdote nivel 80", - 1408: "¡Primero del reino! Humano nivel 80", - 1407: "¡Primero del reino! Enano nivel 80", - 1409: "¡Primero del reino! Elfo de la noche nivel 80", - 1404: "¡Primero del reino! Gnomo nivel 80", - 1406: "¡Primero del reino! Draenei nivel 80", - 1410: "¡Primero del reino! Orco nivel 80", - 1413: "¡Primero del reino! Renegado nivel 80", - 1411: "¡Primero del reino! Tauren nivel 80", - 1412: "¡Primero del reino! Trol nivel 80", - 1405: "¡Primero del reino! Elfo de sangre nivel 80" - } - return achievement_names.get(achievement_id, "Logro Desconocido") - - -def novawow_players_view(request): - # Verificar si el usuario está logueado - if 'username' not in request.session: - return redirect('index') - - # Obtener las 5 hermandades con más miembros - with connections['acore_characters'].cursor() as cursor: - guild_query = """ - SELECT g.name, c.name AS leader, - CASE WHEN c.race IN (1, 3, 4, 7, 11) THEN 'alliance' ELSE 'horde' END AS faction, - COUNT(gm.guid) AS members, - g.createdate - FROM guild g - JOIN characters c ON c.guid = g.leaderguid - LEFT JOIN guild_member gm ON gm.guildid = g.guildid - GROUP BY g.guildid - ORDER BY members DESC - LIMIT 5 - """ - cursor.execute(guild_query) - guilds = cursor.fetchall() - - formatted_guilds = [ - (name, leader, faction, members, datetime.fromtimestamp(createdate).strftime('%d-%m-%Y')) - for name, leader, faction, members, createdate in guilds - ] - - # Obtener el top 10 de jugadores con más muertes con honor - with connections['acore_characters'].cursor() as cursor: - kills_query = """ - SELECT name, race, class, totalKills, todayKills - FROM characters - ORDER BY totalKills DESC - LIMIT 10 - """ - cursor.execute(kills_query) - top_kills = cursor.fetchall() - - players_with_kills = [ - { - 'name': name, - 'race_icon': f"big-{get_race_icon(race)}", - 'class_icon': f"{get_class_icon(char_class)}-medium", - 'total_kills': total_kills, - 'today_kills': today_kills - } - for name, race, char_class, total_kills, today_kills in top_kills - ] - - # Obtener personajes por clase y facción - with connections['acore_characters'].cursor() as cursor: - class_faction_query = """ - SELECT class, - SUM(CASE WHEN race IN (1, 3, 4, 7, 11) THEN 1 ELSE 0 END) AS alliance_count, - SUM(CASE WHEN race IN (2, 5, 6, 8, 10) THEN 1 ELSE 0 END) AS horde_count, - COUNT(*) AS total_count - FROM characters - GROUP BY class - """ - cursor.execute(class_faction_query) - class_faction_data = cursor.fetchall() - - classes_data = [ - { - 'class_icon': f"{get_class_icon(char_class)}-chest", - 'class_name': get_class_name(char_class), - 'total': total_count, - 'alliance': alliance_count, - 'horde': horde_count - } - for char_class, alliance_count, horde_count, total_count in class_faction_data - ] - - # Obtener el top 10 de jugadores con más logros - with connections['acore_characters'].cursor() as cursor: - achievements_query = """ - SELECT c.name, c.race, c.class, MAX(cap.counter) AS achievement_points - FROM character_achievement_progress cap - JOIN characters c ON cap.guid = c.guid - GROUP BY c.guid - ORDER BY achievement_points DESC - LIMIT 10 - """ - cursor.execute(achievements_query) - top_achievements = cursor.fetchall() - - players_with_achievements = [ - { - 'name': name, - 'race_icon': f"big-{get_race_icon(race)}", - 'class_icon': f"{get_class_icon(char_class)}-medium", - 'achievement_points': achievement_points - } - for name, race, char_class, achievement_points in top_achievements - ] - - # Obtener logros "Primeros del Reino" - with connections['acore_characters'].cursor() as cursor: - first_realm_query = """ - SELECT ca.achievement, c.name, c.race, c.class, c.gender, ca.date - FROM character_achievement ca - JOIN characters c ON ca.guid = c.guid - WHERE ca.achievement IN ( - 457, 458, 463, 461, 462, 467, 466, 459, 460, 465, 464, - 1408, 1407, 1409, 1404, 1406, 1410, 1413, 1411, 1412, 1405 - ) - ORDER BY ca.date ASC - """ - cursor.execute(first_realm_query) - first_realm_achievements = cursor.fetchall() - - first_realm_data = [] - for achievement in first_realm_achievements: - achievement_id, name, race, char_class, gender, date = achievement - formatted_date = datetime.fromtimestamp(date).strftime('%H:%M:%S %d-%m-%Y') - - # Determinar el ícono correcto basado en el logro - if achievement_id == 457: - icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-icons/achievement-level-80.jpg" - elif achievement_id in [1408, 1407, 1409, 1404, 1406, 1410, 1413, 1411, 1412, 1405]: - race_iconss = get_race_iconss(race) - gender_suffix = "male" if gender == 0 else "female" - icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-races/big-{race_iconss}-{gender_suffix}.webp" - else: - icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-classes/{get_class_icon(char_class)}-medium.jpg" - - first_realm_data.append({ - 'achievement_name': get_achievement_name(achievement_id), - 'name': name, - 'race_iconss': f"big-{get_race_iconss(race)}", - 'icon_url': icon_url, - 'date': formatted_date - }) - - # Renderizar la plantilla con los datos obtenidos - return render(request, 'realms/novawow_players.html', { - 'guilds': formatted_guilds, - 'players_with_kills': players_with_kills, - 'players_with_achievements': players_with_achievements, - 'classes_data': classes_data, - 'first_realm_data': first_realm_data - }) - - -# --------------------------------------------------------------------------- -# Battlepay (tienda nativa del cliente) pagado vía SumUp -# -# Flujo: el worldserver crea una orden PENDING en `battlepay_orders` cuando el -# jugador pulsa "Comprar" en la tienda del cliente y el producto tiene precio en -# `battlepay_price`. El portal muestra esas órdenes, cobra por SumUp y las marca -# PAID; el worldserver las entrega y las marca DELIVERED. -# --------------------------------------------------------------------------- - -def get_pending_battlepay_orders(account_id): - """Órdenes Battlepay PENDING de una cuenta de juego.""" - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "SELECT id, reference, product_id, product_name, price_eur, status, created_at " - "FROM battlepay_orders WHERE account_id = %s AND status = 'PENDING' " - "ORDER BY created_at DESC", - [account_id], - ) - cols = [c[0] for c in cursor.description] - return [dict(zip(cols, row)) for row in cursor.fetchall()] - - -def _mark_order_paid_by_reference(reference): - """Marca una orden como PAID solo si estaba PENDING. Devuelve filas afectadas.""" - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "UPDATE battlepay_orders SET status = 'PAID', paid_at = UNIX_TIMESTAMP() " - "WHERE reference = %s AND status = 'PENDING'", - [reference], - ) - return cursor.rowcount - - -def battlepay_view(request): - """Lista las órdenes Battlepay pendientes de la cuenta de juego seleccionada.""" - account_id = request.session.get('account_id') - if not account_id: - if request.session.get('bnet_id'): - return redirect('select-account') - return redirect('login') - - orders = get_pending_battlepay_orders(account_id) - return render(request, 'account/battlepay.html', { - 'orders': orders, - 'username': request.session.get('username'), - }) - - -def battlepay_pay_view(request): - """Crea un checkout de SumUp para pagar una orden Battlepay pendiente.""" - account_id = request.session.get('account_id') - if not account_id: - return redirect('login') - if request.method != 'POST': - return redirect('battlepay') - - reference = request.POST.get('reference', '').strip() - - # Validar que la orden pertenece a la cuenta y sigue pendiente - with connections['acore_auth'].cursor() as cursor: - cursor.execute( - "SELECT product_name, price_eur FROM battlepay_orders " - "WHERE reference = %s AND account_id = %s AND status = 'PENDING'", - [reference, account_id], - ) - row = cursor.fetchone() - - if not row: - return render(request, 'account/pago_sumup.html', {'error': 'Orden no encontrada o ya procesada.'}) - - product_name, price_eur = row - try: - checkout = crear_checkout_battlepay(reference, price_eur, product_name) - except Exception as e: - return render(request, 'account/pago_sumup.html', {'error': str(e)}) - - return render(request, 'account/pago_sumup.html', { - 'checkout_id': checkout.get('id'), - 'reference': reference, - 'product_name': product_name, - 'price_eur': price_eur, - }) - - -def pagar_sumup(request): - """Alias de compatibilidad: envía al listado de órdenes Battlepay.""" - return redirect('battlepay') - - -@csrf_exempt -def sumup_webhook(request): - """Webhook de SumUp: marca la orden Battlepay como PAID al completarse el pago.""" - if request.method != "POST": - return JsonResponse({"error": "Método no permitido"}, status=405) - - try: - data = json.loads(request.body or b'{}') - except json.JSONDecodeError: - return JsonResponse({"error": "JSON inválido"}, status=400) - - payload = data.get('payload') or data.get('data') or {} - reference = data.get('checkout_reference') or payload.get('checkout_reference') - status = (data.get('status') or payload.get('status') or '').upper() - checkout_id = data.get('id') or payload.get('checkout_id') or payload.get('id') - event_type = (data.get('event_type') or '').lower() - - # Si falta la referencia o el estado, consultarlo a la API de SumUp - if (not reference or status not in ('PAID', 'SUCCESSFUL')) and checkout_id: - try: - checkout = obtener_checkout(checkout_id) - reference = reference or checkout.get('checkout_reference') - status = (checkout.get('status') or status).upper() - except Exception as e: - logger.error("SumUp webhook: no se pudo consultar el checkout %s: %s", checkout_id, e) - - pagado = status in ('PAID', 'SUCCESSFUL') or event_type == 'transaction.successful' - - if reference and pagado: - try: - afectadas = _mark_order_paid_by_reference(reference) - except Exception as e: - logger.error("SumUp webhook: error actualizando la orden %s: %s", reference, e) - return JsonResponse({"error": "Error interno"}, status=500) - if afectadas: - return JsonResponse({"message": "Orden marcada como pagada"}, status=200) - return JsonResponse({"message": "Orden no pendiente o inexistente"}, status=200) - - return JsonResponse({"message": "Evento ignorado"}, status=200) - \ No newline at end of file diff --git a/home/views/__init__.py b/home/views/__init__.py new file mode 100644 index 0000000..92b2d3a --- /dev/null +++ b/home/views/__init__.py @@ -0,0 +1,18 @@ +"""Paquete de vistas de NovaWoW (antes un único home/views.py de ~3200 líneas). + +Dividido por secciones. El `import *` de cada submódulo re-exporta las vistas +para que `home.views.`, `from home.views import ` y la referencia +por cadena `'home.views.not_found'` (handler404) sigan funcionando igual. +""" +from ._base import * # noqa: F401,F403 +from .auth import * # noqa: F401,F403 +from .pages import * # noqa: F401,F403 +from .account import * # noqa: F401,F403 +from .recruit import * # noqa: F401,F403 +from .characters import * # noqa: F401,F403 +from .store import * # noqa: F401,F403 +from .points import * # noqa: F401,F403 +from .guild import * # noqa: F401,F403 +from .history import * # noqa: F401,F403 +from .players import * # noqa: F401,F403 +from .payments import * # noqa: F401,F403 diff --git a/home/views/_base.py b/home/views/_base.py new file mode 100644 index 0000000..c77094e --- /dev/null +++ b/home/views/_base.py @@ -0,0 +1,68 @@ +"""Imports y utilidades compartidas por el paquete de vistas (`home.views`). + +Cada submódulo de vistas hace `from ._base import *` para heredar estos imports +y el decorador `require_paid_stripe`. +""" +import socket, hashlib, gmpy2, binascii, os, re, json, logging, secrets +from collections import Counter +from datetime import datetime, timedelta +from decimal import Decimal, ROUND_HALF_UP +from functools import wraps + +from django import forms +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import logout +from django.db import connections +from django.http import HttpResponse, JsonResponse +from django.shortcuts import render, redirect +from django.urls import reverse +from django.utils import timezone +from django.utils.crypto import get_random_string +from django.utils.timezone import now +from django.views.decorators.csrf import csrf_exempt +import stripe + +from ..ac_soap import execute_soap_command +from ..library_correo import enviar_correo +from ..pagos_stripe import create_checkout_session, is_session_paid +from ..pagos_sumup import crear_checkout, crear_checkout_battlepay, obtener_checkout +from ..zone_definitions import get_zone_name +from .. import bnet +from ..models import ( + Noticia, ClienteCategoria, ServerSelection, RecruitAFriend, DownloadClientPage, + ContentCreator, RecruitReward, ClaimedReward, AccountActivation, SecurityToken, + GuildRenameSettings, VoteSite, VoteLog, HomeApiPoints, UnstuckHistory, ReviveHistory, + RenamePrice, CustomizePrice, ChangeRacePrice, ChangeFactionPrice, LevelUpPrice, + GoldPrice, TransferPrice, Category, Item, StripeLog, LoginAttempt, Pedido, +) + +# Configuración del logger +logger = logging.getLogger(__name__) + + +def require_paid_stripe(redirect_to='index'): + """ + Protege las vistas de "éxito" de pago: exige un `session_id` de Stripe + realmente PAGADO en la URL (Stripe lo añade tras el pago) y evita re-entregas + marcando el StripeLog como `fulfilled`. Cierra el bypass de ir directo a la + success-url sin pagar. + """ + def deco(view): + @wraps(view) + def wrapper(request, *args, **kwargs): + session_id = request.GET.get('session_id') + if not is_session_paid(session_id): + messages.error(request, 'No se ha podido verificar el pago.') + return redirect(redirect_to) + log = StripeLog.objects.filter(session_id=session_id).first() + if log is not None and log.fulfilled: + messages.info(request, 'Este pago ya había sido procesado.') + return redirect(redirect_to) + response = view(request, *args, **kwargs) + if log is not None: + log.fulfilled = True + log.save(update_fields=['fulfilled']) + return response + return wrapper + return deco diff --git a/home/views/account.py b/home/views/account.py new file mode 100644 index 0000000..0bfbf72 --- /dev/null +++ b/home/views/account.py @@ -0,0 +1,268 @@ +from ._base import * + +dias_semana = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'] +meses = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] + + +def get_class_css(class_id): + class_map = { + 1: 'warrior', + 2: 'paladin', + 3: 'hunter', + 4: 'rogue', + 5: 'priest', + 6: 'death-knight', + 7: 'shaman', + 8: 'mage', + 9: 'warlock', + 11: 'druid' + } + return class_map.get(class_id, 'unknown') +def get_class_text_css(class_id): + text_class_map = { + 1: 'warrior', + 2: 'paladin', + 3: 'hunter', + 4: 'rogue', + 5: 'priest', + 6: 'death-knight', + 7: 'shaman', + 8: 'mage', + 9: 'warlock', + 11: 'druid' + } + return text_class_map.get(class_id, 'unknown') +def formatear_fecha(fecha): + """Formatea la fecha al estilo solicitado.""" + dia_semana = dias_semana[fecha.weekday()] + mes = meses[fecha.month - 1] + return f"{dia_semana} {fecha.day:02d} de {mes} del {fecha.year}" +def my_account(request): + # Verificar si la sesión contiene la cuenta de juego seleccionada + username = request.session.get('username') + + # Si aún no se ha elegido cuenta de juego pero hay login Battle.net, + # enviar al selector; si no, al inicio de sesión. + if not username: + if request.session.get('bnet_id'): + return redirect('select-account') + return redirect('login') + + is_logged_in = True # Si el usuario está en la sesión, asumimos que está conectado + + # Obtener información básica de la cuenta + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, username, reg_mail, email, last_ip, last_attempt_ip, joindate + FROM account + WHERE username = %s + """, [username]) + account_data = cursor.fetchone() + + if not account_data: + return redirect('index') + + account_id = account_data[0] + + # Obtener los personajes asociados a la cuenta + characters = get_account_characters(account_id) + has_characters = bool(characters) + + # Obtener el estado de la cuenta + account_status = get_account_status(account_id) + + # Obtener los puntos (DP y VP) del usuario desde la tabla `home_api_points` + user_points = HomeApiPoints.objects.filter(accountID=account_id).first() + dp = user_points.dp if user_points else 0 + vp = user_points.vp if user_points else 0 + + + # Obtener el estado del token de seguridad + security_token = SecurityToken.objects.filter(user_id=account_id).first() + if security_token: + token_status = "Solicitado" + token_date = security_token.created_at.strftime('%H:%M:%S %d-%m-%Y') + else: + token_status = "Sin solicitar" + token_date = None + + # Créditos Battlepay de la cuenta Battle.net + with connections['acore_auth'].cursor() as cursor: + battlepay_credits = bnet.get_battlepay_credits(cursor, request.session.get('bnet_id')) + + return render(request, 'my-account/my-account.html', { + 'is_logged_in': is_logged_in, + 'battlepay_credits': battlepay_credits, + 'user_info': { + 'username': account_data[1], + 'reg_mail': account_data[2], + 'email': account_data[3], + 'last_ip': account_data[4], + 'last_attempt_ip': account_data[5], + 'joindate': formatear_fecha(account_data[6]), + + }, + 'account_status': account_status, + 'characters': characters, + 'has_characters': has_characters, + 'dp': dp, + 'vp': vp, + 'token_status': token_status, + 'token_date': token_date + }) +def get_account_status(account_id): + with connections['acore_auth'].cursor() as cursor: + # Obtener información de la tabla 'account' + cursor.execute(""" + SELECT username, reg_mail, email, last_ip, last_attempt_ip, joindate, recruiter + FROM account + WHERE id = %s + """, [account_id]) + account_data = cursor.fetchone() + + if not account_data: + return None + + # Verificar si la cuenta está baneada + cursor.execute(""" + SELECT unbandate, UNIX_TIMESTAMP() + FROM account_banned + WHERE id = %s AND active = 1 + """, [account_id]) + ban_data = cursor.fetchone() + + # Obtener el número de amigos reclutados + cursor.execute(""" + SELECT id + FROM account + WHERE recruiter = %s + """, [account_id]) + recruited_accounts = cursor.fetchall() + recruited_count = len(recruited_accounts) + + # Contar cuántos personajes de amigos reclutados han alcanzado el nivel 80 + if recruited_count > 0: + recruited_ids = [acc[0] for acc in recruited_accounts] + with connections['acore_characters'].cursor() as char_cursor: + char_cursor.execute(""" + SELECT COUNT(*) + FROM characters + WHERE account IN %s AND level = 80 + """, [tuple(recruited_ids)]) + level_80_count = char_cursor.fetchone()[0] + else: + level_80_count = 0 + + # Asignar los datos a variables + user_info = { + 'username': account_data[0] if account_data[0] else 'No disponible', + 'reg_mail': account_data[1] if account_data[1] else 'No disponible', + 'email': account_data[2] if account_data[2] else 'No disponible', + 'last_ip': account_data[3] if account_data[3] else 'Nunca', + 'last_attempt_ip': account_data[4] if account_data[4] else 'Nunca', + 'joindate': account_data[5].strftime("%A %d de %B del %Y a las %H:%M:%S Horas") if account_data[5] else 'No disponible', + 'is_recruited': bool(account_data[6]), + 'recruited_count': recruited_count, + 'recruited_level_80_count': level_80_count + } + + # Información sobre la suspensión, si existe + if ban_data: + unban_timestamp, current_timestamp = ban_data + remaining_time = unban_timestamp - current_timestamp + user_info['is_banned'] = True + user_info['unban_date'] = datetime.fromtimestamp(unban_timestamp).strftime("%A %d de %B del %Y a las %H:%M:%S Horas") + user_info['remaining_time'] = remaining_time + else: + user_info['is_banned'] = False + + return user_info +def get_character_image(class_id, race, gender): + """ + Devuelve la imagen del personaje basada en la raza, clase y género. + `gender`: 0 = Hombre, 1 = Mujer + """ + # Definir los nombres de archivos según el género + gender_suffix = 'male' if gender == 0 else 'female' + + # Raza y clases compatibles + race_class_map = { + 10: [2, 3, 4, 5, 6, 8, 9], # Elfo de Sangre + 8: [1, 3, 4, 5, 6, 7, 8], # Trol + 6: [1, 3, 6, 7, 11], # Tauren + 5: [1, 4, 5, 6, 8, 9], # No-Muerto + 2: [1, 3, 4, 6, 7, 9], # Orco + 11: [1, 2, 3, 5, 6, 7, 8], # Draenei + 7: [1, 4, 6, 8, 9], # Gnomo + 4: [1, 3, 4, 5, 6, 11], # Elfo de la Noche + 3: [1, 2, 3, 4, 5, 6], # Enano + 1: [1, 2, 4, 5, 6, 8, 9] # Humano + } + + # Verificar si la clase es compatible con la raza + if class_id not in race_class_map.get(race, []): + return 'nw-themes/nw-ryu/nw-images/nw-classes/unknown.webp' # Imagen por defecto si no es compatible + + # Construir el nombre del archivo basado en la raza y el género + race_image_map = { + 10: f"big-blood-elf-{gender_suffix}.webp", + 8: f"big-troll-{gender_suffix}.webp", + 6: f"big-tauren-{gender_suffix}.webp", + 5: f"big-undead-{gender_suffix}.webp", + 2: f"big-orc-{gender_suffix}.webp", + 11: f"big-draenei-{gender_suffix}.webp", + 7: f"big-gnome-{gender_suffix}.webp", + 4: f"big-night-elf-{gender_suffix}.webp", + 3: f"big-dwarf-{gender_suffix}.webp", + 1: f"big-human-{gender_suffix}.webp" + } + + # Devolver la imagen según la raza y género, o una imagen por defecto + return f"nw-themes/nw-ryu/nw-images/nw-races/{race_image_map.get(race, 'unknown.webp')}" +def get_account_characters(account_id): + """Obtiene los personajes de la cuenta dada.""" + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name, level, money, race, class, zone, gender + FROM characters + WHERE account = %s + """, [account_id]) + characters = cursor.fetchall() + + character_list = [] + for char in characters: + name, level, money, race, class_id, zone, gender = char + gold = money // 10000 + silver = (money % 10000) // 100 + copper = money % 100 + + # Obtener la clase CSS y la imagen antes de pasarla a la plantilla + class_css = get_class_css(class_id) + image_url = get_character_image(class_id, race, gender) + + # Obtener el nombre de la zona usando la función get_zone_name + zone_name = get_zone_name(zone) + + character_list.append({ + 'name': name, + 'level': level, + 'gold': gold, + 'silver': silver, + 'copper': copper, + 'race': race, + 'class_id': class_id, + 'zone': zone_name, + 'class_css': class_css, + 'image_url': image_url + }) + return character_list +def get_account_id(username): + # Consulta para obtener el account_id a partir del username + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id + FROM account + WHERE username = %s + """, [username]) + result = cursor.fetchone() + return result[0] if result else None diff --git a/home/views/auth.py b/home/views/auth.py new file mode 100644 index 0000000..4b758f5 --- /dev/null +++ b/home/views/auth.py @@ -0,0 +1,614 @@ +from ._base import * + + +class LoginForm(forms.Form): + email = forms.EmailField(max_length=320, required=True) + password = forms.CharField(widget=forms.PasswordInput, required=True) +def sha1(data): + """Función auxiliar para calcular SHA-1.""" + return hashlib.sha1(data).digest() +def calculate_srp6_verifier(username, password, salt): + """Calcula el verifier usando SRP6 basado en la implementación PHP.""" + g = gmpy2.mpz(7) + N = gmpy2.mpz('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 16) + + # Paso 1: Calcular H(username:password) + h1 = sha1((username.upper() + ':' + password.upper()).encode('utf-8')) + + # Usar el salt proporcionado (sin invertir) + salt_bytes = bytes.fromhex(salt) + + # Paso 2: Calcular H(salt + H(username:password)) + h2 = sha1(salt_bytes + h1) + + # Convertir el hash a un entero (little-endian) + h2_int = gmpy2.mpz.from_bytes(h2, 'little') + + # Calcular el verifier usando g^h2 mod N + verifier = gmpy2.powmod(g, h2_int, N) + verifier_bytes = verifier.to_bytes(32, 'little') + + # Convertir el resultado a hexadecimal en mayúsculas + return binascii.hexlify(verifier_bytes).decode('utf-8').upper() +def authenticate(email, password): + """Autentica una cuenta Battle.net por email (SRP6 v2).""" + try: + with connections['acore_auth'].cursor() as cursor: + account = bnet.get_bnet_account_by_email(cursor, email) + if not account: + return None + if bnet.bnet_verify(account['email'], password, account['salt'], account['verifier']): + return account + return None + except Exception: + return None +def _set_game_account_session(request, game_account): + """Fija en sesión la cuenta de juego seleccionada (para las demás vistas).""" + request.session['username'] = game_account['username'].upper() + request.session['account_id'] = game_account['id'] +def login_view(request): + if request.method == 'POST': + form = LoginForm(request.POST) + if form.is_valid(): + email = form.cleaned_data['email'] + password = form.cleaned_data['password'] + ip_address = request.META.get('REMOTE_ADDR') + + account = authenticate(email, password) + if account: + log_login_attempt(email, "Exito", ip_address) + + # Guardar la identidad Battle.net en la sesión + request.session['bnet_id'] = account['id'] + request.session['bnet_email'] = account['email'] + + # Resolver las cuentas de juego enlazadas + with connections['acore_auth'].cursor() as cursor: + game_accounts = bnet.get_game_accounts_for_bnet(cursor, account['id']) + + if len(game_accounts) == 1: + # Una sola cuenta de juego: seleccionar automáticamente + _set_game_account_session(request, game_accounts[0]) + elif len(game_accounts) > 1: + # Varias: dejar la selección pendiente (my-account redirige al selector) + request.session.pop('username', None) + request.session.pop('account_id', None) + # 0 cuentas: se queda sin username; el selector mostrará el aviso + + return JsonResponse({'success': True}) + else: + log_login_attempt(email, "Fracaso", ip_address) + return JsonResponse({'success': False, 'error': "Correo o contraseña incorrectos"}) + else: + return JsonResponse({'success': False, 'error': "Formulario no valido"}) + + form = LoginForm() + return render(request, 'auth/login.html', {'form': form}) +def select_account_view(request): + """Selector de cuenta de juego para cuentas Battle.net con varias.""" + bnet_id = request.session.get('bnet_id') + if not bnet_id: + return redirect('login') + + with connections['acore_auth'].cursor() as cursor: + game_accounts = bnet.get_game_accounts_for_bnet(cursor, bnet_id) + + if request.method == 'POST': + try: + selected_id = int(request.POST.get('account_id', '')) + except (TypeError, ValueError): + selected_id = None + chosen = next((g for g in game_accounts if g['id'] == selected_id), None) + if chosen: + _set_game_account_session(request, chosen) + return redirect('my-account') + messages.error(request, 'Cuenta de juego no válida.') + + # Autoseleccionar si solo hay una + if len(game_accounts) == 1: + _set_game_account_session(request, game_accounts[0]) + return redirect('my-account') + + return render(request, 'auth/select_account.html', { + 'game_accounts': game_accounts, + 'bnet_email': request.session.get('bnet_email'), + }) +def logout_view(request): + for key in ('username', 'account_id', 'bnet_id', 'bnet_email'): + request.session.pop(key, None) + return redirect('index') +def register_view(request): + if request.method == 'POST': + password = request.POST.get('password', '').strip() + conf_password = request.POST.get('conf-password', '').strip() + email = request.POST.get('email', '').strip() + conf_email = request.POST.get('conf-email', '').strip() + recruiter = request.POST.get('recruiter', '').strip() + + # En 3.4.3 la identidad es el email (cuenta Battle.net); no hay username. + if not password or not email: + return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) + + if password != conf_password: + return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'}) + + if len(password) > 16: + return JsonResponse({'success': False, 'message': 'La contraseña no debe exceder los 16 caracteres.'}) + + if email != conf_email or not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', email): + return JsonResponse({'success': False, 'message': 'El correo electrónico no es válido.'}) + + # Verificar si ya existe una cuenta Battle.net con ese email + email_norm = bnet.normalize_email(email) + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT COUNT(*) FROM battlenet_accounts WHERE email = %s", [email_norm]) + if cursor.fetchone()[0] > 0: + return JsonResponse({'success': False, 'message': 'Ya existe una cuenta con ese correo electrónico.'}) + + # Comprobar que no haya un registro de activación pendiente para ese email + if AccountActivation.objects.filter(email=email, old_email__isnull=True).exists(): + AccountActivation.objects.filter(email=email, old_email__isnull=True).delete() + + # Validar y obtener recruiter_id (id de cuenta de juego del reclutador) + recruiter_id = 0 + if recruiter: + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id FROM account WHERE username = %s", [recruiter]) + recruiter_data = cursor.fetchone() + if recruiter_data: + recruiter_id = recruiter_data[0] + else: + with connections['acore_characters'].cursor() as char_cursor: + char_cursor.execute("SELECT account FROM characters WHERE name = %s", [recruiter]) + character_data = char_cursor.fetchone() + if character_data: + recruiter_id = character_data[0] + else: + return JsonResponse({'success': False, 'message': 'El reclutador ingresado no existe.'}) + + # Generar un hash único para activación. Los salt/verifier se calculan al + # activar (tanto el de bnet como el de la cuenta de juego). + activation_hash = get_random_string(32) + + AccountActivation.objects.create( + email=email, + password=password, + recruiter_id=recruiter_id, + hash=activation_hash + ) + + # Enviar correo de activación + activation_link = f"{settings.URL_PRINCIPAL}/es/activate-account?act={activation_hash}" + context = { + 'username': email, + 'password': password, + 'activation_link': activation_link, + 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR + } + enviar_correo( + subject=f'Activación de la cuenta {email} - {settings.NOMBRE_SERVIDOR}', + to_email=email, + template='emails/activation.html', + context=context + ) + + message = f""" +
+ La cuenta '{email}' ha sido creada.

+ Se ha enviado un enlace de activación al correo {email}. +
+ """ + return JsonResponse({'success': True, 'message': message}) + + return render(request, 'auth/register.html') +def activate_account_view(request): + activation_hash = request.GET.get('act') + + try: + activation = AccountActivation.objects.get(hash=activation_hash) + + if activation.is_expired(): + return render(request, 'auth/activation_invalid.html') + + email_norm = bnet.normalize_email(activation.email) + password = activation.password + last_ip = request.META.get('REMOTE_ADDR') + + with connections['acore_auth'].cursor() as cursor: + # Evitar duplicados si el enlace se usa dos veces + cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [email_norm]) + existing = cursor.fetchone() + if existing: + activation.delete() + return render(request, 'auth/activation_success.html') + + # 1) Crear la cuenta Battle.net (SRP6 v2) + bnet_salt, bnet_verifier, srp_version = bnet.bnet_make_registration(email_norm, password) + cursor.execute( + "INSERT INTO battlenet_accounts (email, srp_version, salt, verifier) " + "VALUES (%s, %s, %s, %s)", + [email_norm, srp_version, bnet_salt, bnet_verifier] + ) + + # 2) Recuperar el id de la cuenta bnet recién creada + cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [email_norm]) + bnet_id = cursor.fetchone()[0] + + # 3) Crear la cuenta de juego enlazada (#1, SRP6 Grunt) + game_username = bnet.make_game_account_username(bnet_id, 1) + game_salt, game_verifier = bnet.game_make_registration(game_username, password) + cursor.execute( + "INSERT INTO account " + "(username, salt, verifier, email, reg_mail, recruiter, joindate, " + " last_ip, expansion, battlenet_account, battlenet_index) " + "VALUES (%s, %s, %s, %s, %s, %s, NOW(), %s, %s, %s, %s)", + [ + game_username, game_salt, game_verifier, + email_norm, email_norm, activation.recruiter_id or 0, + last_ip, 2, bnet_id, 1 + ] + ) + + activation.delete() + return render(request, 'auth/activation_success.html') + + except AccountActivation.DoesNotExist: + return render(request, 'auth/activation_invalid.html') +def recover_account_view(request): + return render(request, 'auth/recover_account.html') +def change_password_view(request): + # Verificar si el usuario está autenticado mediante la sesión + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener el usuario desde la base de datos de `acore_auth` + user_data = get_user_from_acore(username) + if not user_data: + return redirect('login') + + user_id = user_data['id'] + email = user_data['email'] + + if request.method == 'POST': + current_password = request.POST.get('cur-password', '').strip() + new_password = request.POST.get('new-password', '').strip() + conf_new_password = request.POST.get('conf-new-password', '').strip() + security_token = request.POST.get('security-token', '').strip() + + # Validar campos vacíos + if not current_password or not new_password or not conf_new_password or not security_token: + return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) + + # Validar que la nueva contraseña coincida con su confirmación + if new_password != conf_new_password: + return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'}) + + # Validar longitud de la nueva contraseña + if len(new_password) > 16: + return JsonResponse({'success': False, 'message': 'La contraseña nueva no debe exceder los 16 caracteres.'}) + + # Verificar si el token de seguridad ha sido generado + existing_token = SecurityToken.objects.filter(user_id=user_id).first() + if not existing_token: + return JsonResponse({'success': False, 'message': 'No tienes un token de seguridad generado. Por favor, genera uno antes de cambiar tu contraseña.'}) + + # Verificar si el token proporcionado es correcto + if existing_token.token != security_token: + return JsonResponse({'success': False, 'message': 'El token ingresado es incorrecto.'}) + + # Verificar la contraseña actual contra la cuenta Battle.net (por email) + if not authenticate(email, current_password): + return JsonResponse({'success': False, 'message': 'La contraseña actual es incorrecta.'}) + + # Generar salt y verifier SRP6 v2 para la nueva contraseña (bnet) + new_salt_bytes, new_verifier_bytes, srp_version = bnet.bnet_make_registration(email, new_password) + + # Localizar la cuenta bnet (por sesión o por email) y actualizarla + bnet_id = request.session.get('bnet_id') + with connections['acore_auth'].cursor() as cursor: + if not bnet_id: + account = bnet.get_bnet_account_by_email(cursor, email) + bnet_id = account['id'] if account else None + if not bnet_id: + return JsonResponse({'success': False, 'message': 'No se ha encontrado la cuenta.'}) + cursor.execute( + "UPDATE battlenet_accounts SET srp_version = %s, salt = %s, verifier = %s WHERE id = %s", + [srp_version, new_salt_bytes, new_verifier_bytes, bnet_id] + ) + + # Cerrar la sesión por seguridad + for key in ('username', 'account_id', 'bnet_id', 'bnet_email'): + request.session.pop(key, None) + + # Redirigir al usuario a la página de inicio de sesión + return JsonResponse({ + 'success': True, + 'message': 'Contraseña cambiada exitosamente. Has sido desconectado por seguridad.', + 'redirect': True + }) + + return render(request, 'account/change_password.html') +def get_user_from_acore(username): + """ + Obtiene la información del usuario desde la base de datos `acore_auth`. + """ + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id, email FROM account WHERE username = %s", [username]) + result = cursor.fetchone() + if result: + return {'id': result[0], 'email': result[1]} + return None +def security_token_view(request): + # Verificar si el usuario está autenticado mediante la sesión + username = request.session.get('username') + if not username: + return redirect('index') + + # Obtener el usuario desde la base de datos de `acore_auth` + user_data = get_user_from_acore(username) + if not user_data: + return redirect('index') + + user_id = user_data['id'] + email = user_data['email'] + ip_address = request.META.get('REMOTE_ADDR') + + # Verificar si existe un token activo para mostrar la fecha en la plantilla + existing_token = SecurityToken.objects.filter(user_id=user_id).first() + + # Obtener la fecha del token o "Sin solicitar" si no hay token + token_date = "Sin solicitar" + if existing_token: + token_date = existing_token.created_at.strftime('%H:%M:%S %d-%m-%Y') + + if request.method == 'POST': + # Verificar si el correo del usuario está vacío + if not email: + # Si no tiene correo, devolver un mensaje de error en formato JSON + return JsonResponse({ + 'success': False, + 'message': f'Debes añadir un correo electrónico a tu cuenta antes de solicitar un Token de seguridad.' + }) + # Verificar si ya existe un token y si se puede generar uno nuevo después de 7 días + if existing_token: + # Comprobar si han pasado al menos 7 días desde la creación del token + days_since_creation = (timezone.now() - existing_token.created_at).days + if days_since_creation < 7: + remaining_days = 7 - days_since_creation + return JsonResponse({ + 'success': False, + 'message': f'Sólo puedes solicitar un nuevo Token de seguridad cada 7 días, faltan {remaining_days} días para generar un nuevo Token.' + }) + + # Generar un nuevo token + token = secrets.token_urlsafe(4)[:6] + expires_at = timezone.now() + timedelta(days=7) + + # Eliminar el token anterior y crear uno nuevo + if existing_token: + existing_token.delete() + + # Crear y guardar el nuevo token + new_token = SecurityToken.objects.create( + user_id=user_id, + token=token, + ip_address=ip_address, + expires_at=expires_at # Esta fecha solo se usa para el control de los 7 días + ) + + # Enviar el correo con el token al usuario + send_security_token_email(email, username, token, ip_address) + + # Obtener la nueva fecha del token + new_token_date = new_token.created_at.strftime('%H:%M:%S %d-%m-%Y') + + # Responder con un mensaje exitoso y la nueva fecha + return JsonResponse({ + 'success': True, + 'message': f'Se ha enviado el Token de seguridad al correo {email}.', + 'token_date': new_token_date + }) + + # Pasar `token_date` al contexto de la plantilla + return render(request, 'account/security_token.html', { + 'token_date': token_date + }) +def send_security_token_email(email, username, token, ip_address): + """ + Envía un correo electrónico con el token de seguridad. + """ + context = { + 'username': username, + 'token': token, + 'ip_address': ip_address, + 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR + } + + # Llamada a tu función personalizada `enviar_correo` + enviar_correo( + subject=f'Token de seguridad de la cuenta {username} - {settings.NOMBRE_SERVIDOR}', + to_email=email, + template='emails/security_token.html', # Asegúrate de que este template exista + context=context + ) +def change_email_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener el usuario desde la base de datos de `acore_auth` + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, email, reg_mail + FROM account + WHERE username = %s + """, [username]) + account_data = cursor.fetchone() + + if not account_data: + return redirect('login') + + user_id, current_email, reg_email = account_data + security_token = SecurityToken.objects.filter(user_id=user_id).first() + + if request.method == 'POST': + print("Datos recibidos:", request.POST) + + current_password = request.POST.get('cur-password', '').strip() + current_email_input = request.POST.get('cur-email', '').strip() + new_email = request.POST.get('new-email', '').strip() + conf_new_email = request.POST.get('conf-new-email', '').strip() + token = request.POST.get('security-token', '').strip() + + # Validar los campos + if not all([current_password, current_email_input, new_email, conf_new_email, token]): + return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) + + if current_email_input != current_email: + return JsonResponse({'success': False, 'message': 'El correo actual no es correcto.'}) + + if new_email != conf_new_email: + return JsonResponse({'success': False, 'message': 'Los correos no coinciden.'}) + + if not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', new_email): + return JsonResponse({'success': False, 'message': 'El nuevo correo no es válido.'}) + + if not security_token or security_token.token != token: + return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) + + # Verificar la contraseña contra la cuenta Battle.net (por email actual) + if not authenticate(current_email, current_password): + return JsonResponse({'success': False, 'message': 'Contraseña incorrecta.'}) + + # Generar hashes + old_email_hash = secrets.token_urlsafe(16) + new_email_hash = secrets.token_urlsafe(16) + + # Guardar en `AccountActivation`. Se conserva la contraseña porque al + # cambiar el email hay que recalcular el verifier de bnet (el usuario SRP + # depende del email). + AccountActivation.objects.create( + username=username, + email=new_email, + old_email=current_email, + password=current_password, + recruiter_id=user_id, + hash=new_email_hash, + old_email_hash=old_email_hash + ) + + # Enviar confirmación al correo actual + confirm_old_email_link = f"{settings.URL_PRINCIPAL}/es/confirm-old-email?hash={old_email_hash}" + context_old_email = { + 'username': username, + 'confirm_link': confirm_old_email_link, + 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR + } + enviar_correo( + subject=f'Confirme el cambio de correo para {username}', + to_email=current_email, + template='emails/confirm_old_email.html', + context=context_old_email + ) + + return JsonResponse({'success': True, 'message': 'Se ha enviado un correo de confirmación al correo actual.'}) + + return render(request, 'account/change_email.html') +def confirm_old_email_view(request): + hash = request.GET.get('hash') + activation = AccountActivation.objects.filter(old_email_hash=hash, is_used=False).first() + + if not activation or activation.is_expired(): + return JsonResponse({ + 'success': False, + 'message': 'Enlace no válido o expirado.', + 'redirect_url': reverse('expired-link') + }) + + # Marcar solo el old_email_hash como usado + activation.is_used = True + activation.save() + + # Enviar correo al nuevo correo para confirmar el cambio + activation_link = f"{settings.URL_PRINCIPAL}/es/confirm-new-email?hash={activation.hash}" + context_new_email = { + 'username': activation.username, + 'activation_link': activation_link, + 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR + } + enviar_correo( + subject=f'Confirme su nuevo correo para {activation.username}', + to_email=activation.email, + template='emails/confirm_new_email.html', + context=context_new_email + ) + + return JsonResponse({'success': True, 'message': 'Correo confirmado. Se ha enviado un enlace al nuevo correo.'}) +def confirm_new_email_view(request): + hash = request.GET.get('hash') + activation = AccountActivation.objects.filter(hash=hash, is_new_email_used=False).first() + + if not activation or activation.is_expired(): + return JsonResponse({ + 'success': False, + 'message': 'Enlace no válido o expirado.', + 'redirect_url': reverse('expired-link') + }) + + # Marcar solo el new_email_hash como usado + activation.is_new_email_used = True + activation.save() + + # Cambiar el email en la cuenta Battle.net implica recalcular salt/verifier, + # porque el usuario SRP = SHA256(UPPER(email)). + new_email_norm = bnet.normalize_email(activation.email) + old_email_norm = bnet.normalize_email(activation.old_email or '') + new_salt, new_verifier, srp_version = bnet.bnet_make_registration(new_email_norm, activation.password) + + with connections['acore_auth'].cursor() as cursor: + # Localizar la cuenta bnet por su email actual (el antiguo) + cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [old_email_norm]) + row = cursor.fetchone() + if not row: + return JsonResponse({ + 'success': False, + 'message': 'No se ha encontrado la cuenta Battle.net.', + }) + bnet_id = row[0] + + # Actualizar email + verifier en battlenet_accounts + cursor.execute( + "UPDATE battlenet_accounts SET email = %s, srp_version = %s, salt = %s, verifier = %s WHERE id = %s", + [new_email_norm, srp_version, new_salt, new_verifier, bnet_id] + ) + # Actualizar el email de las cuentas de juego enlazadas (cosmético) + cursor.execute( + "UPDATE account SET email = %s, reg_mail = %s WHERE battlenet_account = %s", + [new_email_norm, new_email_norm, bnet_id] + ) + + # Enviar notificación al correo anterior + context_old_email = { + 'username': activation.username, + 'new_email': activation.email, + 'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR + } + enviar_correo( + subject=f'Su correo ha sido cambiado en {settings.NOMBRE_SERVIDOR}', + to_email=activation.old_email, + template='emails/old_email_notification.html', + context=context_old_email + ) + + return JsonResponse({'success': True, 'message': 'El cambio de correo ha sido confirmado y completado con éxito.'}) +def expired_link_view(request): + return render(request, 'account/expired_link.html') +def log_login_attempt(username, status, ip_address): + # Registrar intento de inicio de sesión en el historial + LoginAttempt.objects.create( + username=username, + status=status, + ip_address=ip_address, + timestamp=timezone.now() + ) diff --git a/home/views/characters.py b/home/views/characters.py new file mode 100644 index 0000000..f29ecf6 --- /dev/null +++ b/home/views/characters.py @@ -0,0 +1,978 @@ +from ._base import * + + +def rename_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # 🔹 Obtener información de la cuenta desde acore_auth + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, email, last_ip FROM account WHERE username = %s + """, [username]) + account_info = cursor.fetchone() + + if not account_info: + return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) + + account_id, email, acore_ip = account_info + client_ip = request.META.get('REMOTE_ADDR', '') + + # 🔹 Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name FROM characters WHERE account = %s + """, [account_id]) + characters = [row[0] for row in cursor.fetchall()] + + # 🔹 Obtener el precio del renombrado + price_obj = RenamePrice.objects.first() + price = price_obj.price if price_obj else 2.00 + + # 🔹 Ejecutar el comando SOAP para verificar el servidor + command = f".server info" + response = execute_soap_command(command) + + if request.method == 'POST': + character_name = request.POST.get('character') + + if response is None: + return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) + + if not character_name: + return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) + + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para renombrar este personaje.'}) + + # 🔹 Crear sesión de pago con Stripe + success_url = request.build_absolute_uri(reverse('rename-success')) + cancel_url = request.build_absolute_uri(reverse('rename-cancel')) + stripe_response = create_checkout_session( + account_id=account_id, + username=username, + email=email, + acore_ip=acore_ip, + amount=price, + character_name=character_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Renombrar personaje: {character_name}" + ) + + if stripe_response["success"]: + request.session['rename_character'] = character_name + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'account/rename_character.html', {'characters': characters, 'price': price}) +@require_paid_stripe() +def rename_success_view(request): + # Procesar el comando SOAP tras el pago exitoso + character_name = request.session.get('rename_character') + if not character_name: + return redirect('rename-character') + + command = f".char rename {character_name}" + response = execute_soap_command(command) + + if response is None or response.strip() == "": + message = f"El personaje {character_name} ha sido marcado para renombrar." + request.session.pop('rename_character', None) # Limpiar la sesión + return render(request, 'account/rename_success.html', {'message': message}) + else: + return render(request, 'account/rename_success.html', {'message': f"Error: {response}"}) +def rename_cancel_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + return render(request, 'account/rename_cancel.html') +def restore_character_view(request): + # Verifica si el usuario está autenticado y obtiene su nombre de usuario + username = request.session.get('username') + if not username: + return redirect('login') # Redirige si no hay sesión + + # Obtener la ID de cuenta del usuario conectado a partir del sistema de autenticación personalizada + try: + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id FROM account WHERE username = %s", [username]) + result = cursor.fetchone() + + if not result: + # Si no se encuentra la cuenta, mostrar un error + messages.error(request, "No se encontró la cuenta asociada a ese usuario.") + return redirect('login') # Redirige al login si no se encuentra la cuenta + + account_id = result[0] # Obtén el ID de la cuenta + except Exception as e: + messages.error(request, f"Ocurrió un error al recuperar la cuenta: {str(e)}") + return redirect('login') + + # Verificar si se ha seleccionado un personaje para restaurar + if request.method == 'POST': + # Obtener el GUID del personaje seleccionado para restaurar + guid = request.POST.get('guid') + + if not guid: + messages.error(request, "No se seleccionó un personaje para restaurar.") + return redirect('restore-character') + + # Aquí es donde usamos SOAP en lugar de la actualización directa + try: + command = f".char del restore {guid}" # Comando SOAP para restaurar el personaje + response = execute_soap_command(command) # Ejecuta el comando SOAP + + # Si la respuesta es exitosa, informamos al usuario + if response == "success": + messages.success(request, "Personaje restaurado exitosamente.") + return redirect('my-account') # Redirigir a la cuenta del usuario + else: + messages.error(request, "Hubo un error al restaurar el personaje.") + return redirect('restore-character') + + except Exception as e: + messages.error(request, f"Hubo un error al intentar restaurar el personaje: {str(e)}") + return redirect('restore-character') + + # Realizar la consulta para obtener los personajes eliminados de la cuenta + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT guid, level, deleteInfos_Name, deleteInfos_Account, deleteDate + FROM characters + WHERE deleteInfos_Account = %s AND deleteDate IS NOT NULL + """, [account_id]) # Filtra por account_id + + # Obtener los resultados de la consulta + deleted_characters = cursor.fetchall() + + # Si no hay personajes eliminados + if not deleted_characters: + return render(request, 'account/restore_character.html', { + 'message': 'No hay personajes eliminados que puedas recuperar.' + }) + + # Crear un listado de personajes eliminados con su nombre y nivel (si lo tienes) + character_list = [] + for character in deleted_characters: + guid, level, name, account_id, delete_date = character + # Agregar la información del personaje al listado + character_list.append({ + 'name': name, + 'guid': guid, + 'level': level, + 'delete_date': delete_date + }) + + # Renderiza la plantilla con los personajes eliminados + return render(request, 'account/restore_character.html', { + 'character_list': character_list + }) +def restore_items_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + characters = [] + deleted_items = [] + selected_character = None + + try: + # Obtener la ID de cuenta del usuario + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id FROM account WHERE username = %s", [username]) + result = cursor.fetchone() + if result: + account_id = result[0] + else: + messages.error(request, "No se encontró la cuenta asociada.") + return redirect('login') + + # Obtener los personajes del usuario + with connections['acore_characters'].cursor() as cursor: + cursor.execute("SELECT name FROM characters WHERE account = %s", [account_id]) + characters = [row[0] for row in cursor.fetchall()] + + except Exception as e: + messages.error(request, f"Error al obtener los personajes: {str(e)}") + + if request.method == "GET" and "character_name" in request.GET: + selected_character = request.GET.get("character_name") + + if selected_character: + try: + # Ejecutar comando SOAP para obtener ítems eliminados del personaje seleccionado + command = f".item restore list {selected_character}" + response = execute_soap_command(command) + + # Procesar la respuesta del servidor + for line in response.split("\n"): + match = re.search(r"Recover id: (\d+) \| Item: (.+) \((\d+)\) \| Count: (\d+)", line) + if match: + recover_id = match.group(1) + item_name = match.group(2) + item_id = match.group(3) + count = match.group(4) + deleted_items.append({ + "recover_id": recover_id, + "item_name": item_name, + "item_id": item_id, + "count": count + }) + + except Exception as e: + messages.error(request, f"Error al obtener los ítems eliminados: {str(e)}") + + if request.method == "POST": + recover_id = request.POST.get("recover_id") + selected_character = request.POST.get("character_name") + + if not recover_id or not selected_character: + messages.error(request, "Selecciona un personaje y un ítem para restaurar.") + return redirect('restore-items') + + try: + # Ejecutar comando SOAP para restaurar el ítem + command = f".item restore {recover_id} {selected_character}" + response = execute_soap_command(command) + + if "success" in response.lower(): + messages.success(request, "Ítem restaurado exitosamente.") + else: + messages.error(request, "No se pudo restaurar el ítem.") + except Exception as e: + messages.error(request, f"Error al restaurar el ítem: {str(e)}") + + return redirect('restore-items') + + return render(request, 'account/restore_items.html', { + 'characters': characters, + 'deleted_items': deleted_items, + 'selected_character': selected_character + }) +def quest_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + return render(request, 'account/quest_character.html') +def send_gift_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + return render(request, 'account/send_gift.html') +def customize_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # ?? Obtener información de la cuenta desde acore_auth + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, email, last_ip FROM account WHERE username = %s + """, [username]) + account_info = cursor.fetchone() + + if not account_info: + return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) + + account_id, email, acore_ip = account_info + client_ip = request.META.get('REMOTE_ADDR', '') + + # ?? Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name FROM characters WHERE account = %s + """, [account_id]) + + characters = [row[0] for row in cursor.fetchall()] + + # Obtener el precio de la personalización (si se gestiona desde el admin) + price_obj = CustomizePrice.objects.first() + price = price_obj.price if price_obj else 1.00 + + if request.method == 'POST': + character_name = request.POST.get('character') + + # Validar si el personaje fue seleccionado + if not character_name: + return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) + + # Verificar si el personaje pertenece al usuario + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para personalizar este personaje.'}) + + # Crear sesión de pago con Stripe + success_url = request.build_absolute_uri(reverse('customize-success')) + cancel_url = request.build_absolute_uri(reverse('customize-cancel')) + stripe_response = create_checkout_session( + account_id=account_id, + username=username, + email=email, + acore_ip=acore_ip, + amount=price, + character_name=character_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Personalizar personaje: {character_name}" + ) + + + if stripe_response["success"]: + # Guardar temporalmente el nombre del personaje en la sesión + request.session['customize_character'] = character_name + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'account/customize_character.html', {'characters': characters, 'price': price}) +@require_paid_stripe() +def customize_success_view(request): + # Procesar el comando SOAP tras el pago exitoso + character_name = request.session.get('customize_character') + if not character_name: + return redirect('customize-character') + + command = f".char customi {character_name}" + response = execute_soap_command(command) + + if response is None or response.strip() == "": + message = f"El personaje {character_name} ha sido marcado para personalización." + request.session.pop('customize_character', None) # Limpiar la sesión + return render(request, 'account/customize_success.html', {'message': message}) + else: + return render(request, 'account/customize_success.html', {'message': f"Error: {response}"}) +def customize_cancel_view(request): + return render(request, 'account/customize_cancel.html') +def change_race_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, email, last_ip FROM account WHERE username = %s + """, [username]) + account_info = cursor.fetchone() + + if not account_info: + return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) + + account_id, email, acore_ip = account_info + client_ip = request.META.get('REMOTE_ADDR', '') + + # ?? Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name FROM characters WHERE account = %s + """, [account_id]) + characters = [row[0] for row in cursor.fetchall()] + + # Obtener el precio del cambio de raza + price_obj = ChangeRacePrice.objects.first() + price = price_obj.price if price_obj else 10.00 # Precio predeterminado si no hay registro + + if request.method == 'POST': + character_name = request.POST.get('character') + + # Validar si el personaje fue seleccionado + if not character_name: + return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) + + # Verificar si el personaje pertenece al usuario + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para cambiar la raza de este personaje.'}) + + # Crear sesión de pago con Stripe + success_url = request.build_absolute_uri(reverse('change-race-success')) + cancel_url = request.build_absolute_uri(reverse('change-race-cancel')) + stripe_response = create_checkout_session( + account_id=account_id, + username=username, + email=email, + acore_ip=acore_ip, + amount=price, + character_name=character_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Cambio de Raza del personaje: {character_name}" + ) + + if stripe_response["success"]: + # Guardar el personaje en la sesión para el cambio + request.session['change_race_character'] = character_name + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'account/change_race.html', {'characters': characters, 'price': price}) +@require_paid_stripe() +def change_race_success_view(request): + # Procesar el comando SOAP tras el pago exitoso + character_name = request.session.get('change_race_character') + if not character_name: + return redirect('change-race') + + command = f".char changerace {character_name}" + response = execute_soap_command(command) + + if response is None or response.strip() == "": + message = f"El personaje {character_name} ha sido marcado para cambio de raza." + request.session.pop('change_race_character', None) # Limpiar la sesión + return render(request, 'account/change_race_success.html', {'message': message}) + else: + return render(request, 'account/change_race_success.html', {'message': f"Error: {response}"}) +def change_race_cancel_view(request): + return render(request, 'account/change_race_cancel.html') +def change_faction_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, email, last_ip FROM account WHERE username = %s + """, [username]) + account_info = cursor.fetchone() + + if not account_info: + return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) + + account_id, email, acore_ip = account_info + client_ip = request.META.get('REMOTE_ADDR', '') + + # ?? Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name FROM characters WHERE account = %s + """, [account_id]) + characters = [row[0] for row in cursor.fetchall()] + + # Obtener el precio del cambio de facción + price_obj = ChangeFactionPrice.objects.first() + price = price_obj.price if price_obj else 10.00 + + if request.method == 'POST': + character_name = request.POST.get('character') + + # Validar si el personaje fue seleccionado + if not character_name: + return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) + + # Verificar si el personaje pertenece al usuario + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para cambiar la facción de este personaje.'}) + + # Crear sesión de pago con Stripe + success_url = request.build_absolute_uri(reverse('change-faction-success')) + cancel_url = request.build_absolute_uri(reverse('change-faction-cancel')) + stripe_response = create_checkout_session( + account_id=account_id, + username=username, + email=email, + acore_ip=acore_ip, + amount=price, + character_name=character_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Cambiar Faccion del personaje: {character_name}" + ) + + if stripe_response["success"]: + # Guardar el personaje seleccionado en la sesión + request.session['change_faction_character'] = character_name + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'account/change_faction.html', {'characters': characters, 'price': price}) +@require_paid_stripe() +def change_faction_success_view(request): + # Procesar el comando SOAP tras el pago exitoso + character_name = request.session.get('change_faction_character') + if not character_name: + return redirect('change-faction-character') + + command = f".char changef {character_name}" + response = execute_soap_command(command) + + if response is None or response.strip() == "": + message = f"El personaje {character_name} ha cambiado de facción con éxito." + request.session.pop('change_faction_character', None) # Limpiar la sesión + return render(request, 'account/change_faction_success.html', {'message': message}) + else: + return render(request, 'account/change_faction_success.html', {'message': f"Error: {response}"}) +def change_faction_cancel_view(request): + return render(request, 'account/change_faction_cancel.html') +def level_up_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, email, last_ip FROM account WHERE username = %s + """, [username]) + account_info = cursor.fetchone() + + if not account_info: + return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) + + account_id, email, acore_ip = account_info + client_ip = request.META.get('REMOTE_ADDR', '') + + # ?? Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name FROM characters WHERE account = %s + """, [account_id]) + characters = [row[0] for row in cursor.fetchall()] + + # Obtener el precio de subir a nivel 80 + price_obj = LevelUpPrice.objects.first() + price = price_obj.price if price_obj else 10.00 + + if request.method == 'POST': + character_name = request.POST.get('character') + + # Validar si el personaje fue seleccionado + if not character_name: + return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) + + # Verificar si el personaje pertenece al usuario + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para subir de nivel a este personaje.'}) + + # Crear sesión de pago con Stripe + success_url = request.build_absolute_uri(reverse('level-up-success')) + cancel_url = request.build_absolute_uri(reverse('level-up-cancel')) + stripe_response = create_checkout_session( + account_id=account_id, + username=username, + email=email, + acore_ip=acore_ip, + amount=price, + character_name=character_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Subir el Nivel 80 del personaje: {character_name}" + ) + + if stripe_response["success"]: + # Guardar el personaje en la sesión para usarlo después del pago + request.session['levelup_character'] = character_name + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'account/level_up.html', {'characters': characters, 'price': price}) +@require_paid_stripe() +def level_up_success_view(request): + character_name = request.session.get('levelup_character') + if not character_name: + return redirect('level-up-character') + + # Ejecutar el comando SOAP + command = f".char level {character_name} 80" + response = execute_soap_command(command) + + if response is None or response.strip() == "": + message = f"El personaje {character_name} ha sido subido al nivel 80." + else: + message = f"Error: {response}" + + # Limpiar la sesión + request.session.pop('levelup_character', None) + + return render(request, 'account/level_up_success.html', {'message': message}) +def level_up_cancel_view(request): + return render(request, 'account/level_up_cancel.html') +def gold_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener personajes y estado de conexión del usuario + try: + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, email, last_ip FROM account WHERE username = %s + """, [username]) + account_info = cursor.fetchone() + + if not account_info: + return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) + + account_id, email, acore_ip = account_info + client_ip = request.META.get('REMOTE_ADDR', '') + + # Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name, online, money FROM characters WHERE account = %s + """, [account_id]) + characters_data = cursor.fetchall() + + characters = {row[0]: {'online': row[1], 'money': row[2]} for row in characters_data} + + except Exception as e: + return JsonResponse({'success': False, 'message': f'Error al obtener los personajes: {str(e)}'}) + + # Obtener precios del oro + gold_prices = GoldPrice.objects.all().order_by('gold_amount') + + if request.method == 'POST': + character_name = request.POST.get('character') + gold_amount = request.POST.get('gold_amount') + + # Validaciones iniciales + if not character_name or not gold_amount: + return JsonResponse({'success': False, 'message': 'Seleccione un personaje y una cantidad de oro.'}) + + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para enviar oro a este personaje.'}) + + if characters[character_name]['online'] == 1: + return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para recibir oro.'}) + + try: + gold_amount = int(gold_amount) + except ValueError: + return JsonResponse({'success': False, 'message': 'Cantidad de oro no válida.'}) + + gold_price = GoldPrice.objects.filter(gold_amount=gold_amount).first() + if not gold_price: + return JsonResponse({'success': False, 'message': 'Cantidad de oro no válida.'}) + + # El precio va en euros (con decimales); el helper lo pasa a céntimos (×100) + stripe_price = gold_price.price + + # Crear sesión de pago con Stripe + success_url = request.build_absolute_uri(reverse('gold-success')) + cancel_url = request.build_absolute_uri(reverse('gold-cancel')) + + # Llamada corregida a create_checkout_session con todos los parámetros requeridos + stripe_response = create_checkout_session( + account_id=account_id, + username=username, + email=email, + acore_ip=acore_ip, + amount=stripe_price, # El precio calculado + character_name=character_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Aumentar el Oro: {gold_amount} al Personaje: {character_name}" + ) + + if stripe_response["success"]: + # Guardar en sesión los datos para el callback tras el pago + request.session['gold_transaction'] = { + 'character_name': character_name, + 'gold_amount': gold_amount + } + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'account/gold_character.html', {'characters': characters, 'gold_prices': gold_prices}) +@require_paid_stripe() +def gold_success_view(request): + # Procesar el pago exitoso + transaction = request.session.pop('gold_transaction', None) + if not transaction: + return redirect('gold-character') + + character_name = transaction['character_name'] + gold_amount = int(transaction['gold_amount']) + + # Calcular el oro en formato del juego + game_gold_amount = gold_amount * 10000 + + # Actualizar la cantidad de oro en la base de datos + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + UPDATE characters + SET money = money + %s + WHERE name = %s + """, [game_gold_amount, character_name]) + + return render(request, 'account/gold_success.html', {'character_name': character_name, 'gold_amount': gold_amount}) +def gold_cancel_view(request): + return render(request, 'account/gold_cancel.html') +def transfer_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener el precio de transferencia + price_obj = TransferPrice.objects.first() + price = price_obj.price if price_obj else 10.00 + + # Obtener el ID de usuario desde acore_auth.account + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id, email, last_ip FROM account WHERE username = %s", [username]) + user_data = cursor.fetchone() + if not user_data: + return JsonResponse({'success': False, 'message': 'Cuenta no encontrada. Por favor, inicie sesión nuevamente.'}) + + user_id, email, acore_ip = user_data # ID de la cuenta, email y la última IP + + # Obtener personajes de la cuenta de origen + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name, class, online + FROM characters + WHERE account = %s + """, [user_id]) + characters = cursor.fetchall() + + if request.method == 'POST': + character_name = request.POST.get('character') + destination_account = request.POST.get('destination_account') + token = request.POST.get('security_token') + + # Validaciones + if not all([character_name, destination_account, token]): + return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'}) + + character = next((char for char in characters if char[0] == character_name), None) + if not character: + return JsonResponse({'success': False, 'message': 'No tienes permiso para transferir este personaje.'}) + + # Verificar si el personaje está desconectado + if character[2] == 1: # Si está conectado + return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser transferido.'}) + + # Verificar el token de seguridad + security_token = SecurityToken.objects.filter(user_id=user_id, token=token).first() + if not security_token: + return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) + + # Verificar si el token ha expirado + if security_token.expires_at < timezone.now(): + return JsonResponse({'success': False, 'message': 'El token ha expirado. Solicite uno nuevo.'}) + + # Verificar si la cuenta de destino existe + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id FROM account WHERE username = %s", [destination_account]) + destination_account_data = cursor.fetchone() + + if not destination_account_data: + return JsonResponse({'success': False, 'message': 'La cuenta de destino no existe.'}) + + # Verificar restricciones en la cuenta de destino + destination_account_id = destination_account_data[0] + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT account, name, class, level, online + FROM characters + WHERE account = %s + """, [destination_account_id]) + destination_characters = cursor.fetchall() + + if not any(char[3] >= 55 and char[2] != 6 for char in destination_characters): + return JsonResponse({'success': False, 'message': 'La cuenta destino debe tener al menos un personaje de nivel 55 o superior que no sea Caballero de la Muerte.'}) + + if any(char[2] == 6 for char in destination_characters): + return JsonResponse({'success': False, 'message': 'La cuenta destino no puede tener un Caballero de la Muerte.'}) + + # Verificar que ninguna cuenta esté online + for char in destination_characters: + if char[4] == 1: + return JsonResponse({'success': False, 'message': 'Todos los personajes de la cuenta destino deben estar desconectados.'}) + for char in characters: + if char[2] == 1: + return JsonResponse({'success': False, 'message': 'Todos los personajes de la cuenta origen deben estar desconectados.'}) + + # Crear sesión de pago con Stripe + success_url = request.build_absolute_uri(reverse('transfer-success')) + cancel_url = request.build_absolute_uri(reverse('transfer-cancel')) + + # Llamada corregida a create_checkout_session con todos los parámetros requeridos + stripe_response = create_checkout_session( + username=username, + email=email, + acore_ip=acore_ip, + account_id=user_id, # Pasar el account_id + amount=price, # El precio de la transferencia + character_name=character_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Transferir el personaje: {character_name} a la cuenta de destino: {destination_account}" + ) + + if stripe_response["success"]: + # Guardar los datos de la transferencia en la sesión para usarlos después del pago + request.session['transfer_data'] = { + 'character_name': character_name, + 'destination_account': destination_account + } + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'account/transfer_character.html', {'characters': characters, 'price': price}) +@require_paid_stripe() +def transfer_success_view(request): + transfer_data = request.session.get('transfer_data') + if not transfer_data: + return redirect('transfer-character') + + character_name = transfer_data['character_name'] + destination_account = transfer_data['destination_account'] + + # Ejecutar comando SOAP + command = f".char changeaccount {destination_account} {character_name}" + response = execute_soap_command(command) + + if response is None or response.strip() == "": + message = f"El personaje {character_name} ha sido transferido a la cuenta {destination_account}." + request.session.pop('transfer_data', None) # Limpiar sesión + return render(request, 'account/transfer_success.html', {'message': message}) + else: + return render(request, 'account/transfer_success.html', {'message': f"Error: {response}"}) +def transfer_cancel_view(request): + return render(request, 'account/transfer_cancel.html') +def revive_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name, online + FROM characters + WHERE account = ( + SELECT id + FROM acore_auth.account + WHERE username = %s + ) + """, [username]) + characters_data = cursor.fetchall() + characters = {row[0]: row[1] for row in characters_data} # Diccionario con nombre y estado de conexión + + if request.method == 'POST': + character_name = request.POST.get('character') + + # Validar si el personaje fue seleccionado + if not character_name: + return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) + + # Verificar si el personaje pertenece al usuario + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para revivir este personaje.'}) + + # Verificar si el personaje está desconectado + if characters[character_name] == 1: # Si está conectado + return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser revivido.'}) + + # Verificar el historial de uso del comando `revive` + last_revive = ReviveHistory.objects.filter(character_name=character_name).order_by('-used_at').first() + if last_revive and last_revive.used_at > now() - timedelta(hours=12): + return JsonResponse({'success': False, 'message': 'El personaje solo puede revivirse una vez cada 12 horas.'}) + + # Ejecutar el comando SOAP + command = f".revive {character_name}" + response = execute_soap_command(command) + + # None = fallo de comunicación con el servidor + if response is None: + return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) + + # Respuesta vacía = éxito (el comando .revive no devuelve texto si va bien) + if response.strip() == "": + ReviveHistory.objects.create(character_name=character_name, used_at=now()) + return JsonResponse({'success': True, 'message': f'El personaje {character_name} ha sido revivido con éxito.'}) + + # Respuesta no vacía = el servidor devolvió un mensaje/error + return JsonResponse({'success': False, 'message': f'{response}'}) + + return render(request, 'account/revive_character.html', {'characters': characters.keys()}) +def unstuck_character_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener personajes del usuario desde la base de datos `acore_characters` + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name, online + FROM characters + WHERE account = ( + SELECT id + FROM acore_auth.account + WHERE username = %s + ) + """, [username]) + characters_data = cursor.fetchall() + characters = {row[0]: row[1] for row in characters_data} # Diccionario con nombre y estado de conexión + + if request.method == 'POST': + character_name = request.POST.get('character') + + # Validar si el personaje fue seleccionado + if not character_name: + return JsonResponse({'success': False, 'message': 'Por favor, seleccione un personaje.'}) + + # Verificar si el personaje pertenece al usuario + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para desbloquear este personaje.'}) + + # Verificar si el personaje está desconectado + if characters[character_name] == 1: # Si está conectado + return JsonResponse({'success': False, 'message': 'El personaje debe estar desconectado para ser desbloqueado.'}) + + # Verificar el historial de uso del comando + last_unstuck = UnstuckHistory.objects.filter(character_name=character_name).order_by('-used_at').first() + if last_unstuck and last_unstuck.used_at > now() - timedelta(hours=12): + return JsonResponse({'success': False, 'message': 'El personaje solo puede desbloquearse una vez cada 12 horas.'}) + + # Ejecutar el comando SOAP + command = f".unstuck {character_name} startzone" + response = execute_soap_command(command) + + if response is None: + return JsonResponse({'success': False, 'message': 'Error de comunicación con el servidor. Intente nuevamente más tarde.'}) + + # Analizar la respuesta + if f"You are summoning {character_name} (offline)" in response: + # Registrar el uso del comando + UnstuckHistory.objects.create(character_name=character_name, used_at=now()) + return JsonResponse({'success': True, 'message': f'El personaje {character_name} ha sido desbloqueado con éxito.'}) + else: + return JsonResponse({'success': False, 'message': f'Error: {response}'}) + + return render(request, 'account/unstuck_character.html', {'characters': characters.keys()}) diff --git a/home/views/guild.py b/home/views/guild.py new file mode 100644 index 0000000..431f70a --- /dev/null +++ b/home/views/guild.py @@ -0,0 +1,126 @@ +from ._base import * +from .auth import authenticate, get_user_from_acore + + +def rename_guild_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + user_data = get_user_from_acore(username) + if not user_data: + return redirect('login') + + user_id = user_data['id'] + email = user_data['email'] + + security_token = SecurityToken.objects.filter(user_id=user_id).first() + guild_settings, created = GuildRenameSettings.objects.get_or_create(defaults={'cost': 10}) + rename_cost = guild_settings.cost + + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT g.guildid, g.name, gm.rank, c.name + FROM guild g + JOIN guild_member gm ON g.guildid = gm.guildid + JOIN characters c ON c.guid = gm.guid + WHERE c.account = %s AND gm.rank = 0 + """, [user_id]) + guild_leader_data = cursor.fetchall() + + if not guild_leader_data: + return JsonResponse({'success': False, 'message': 'Error: No tienes personajes que sean Maestros de Hermandad en tu cuenta.'}) + + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT id, email, last_ip FROM account WHERE username = %s + """, [username]) + account_info = cursor.fetchone() + + if not account_info: + return JsonResponse({'success': False, 'message': 'Error: Cuenta no encontrada.'}) + + account_id, email, acore_ip = account_info + client_ip = request.META.get('REMOTE_ADDR', '') + + if request.method == 'POST': + old_guild_name = request.POST.get('old-guild-name', '').strip() + new_guild_name = request.POST.get('new-guild-name', '').strip() + conf_new_guild_name = request.POST.get('conf-new-guild-name', '').strip() + current_password = request.POST.get('cur-password', '').strip() + token = request.POST.get('security-token', '').strip() + + if not old_guild_name or not new_guild_name or not conf_new_guild_name or not current_password or not token: + return JsonResponse({'success': False, 'message': 'Por favor, completa todos los campos.'}) + + if not authenticate(email, current_password): + return JsonResponse({'success': False, 'message': 'La contraseña actual es incorrecta.'}) + + if not security_token or security_token.token != token: + return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) + + if new_guild_name != conf_new_guild_name: + return JsonResponse({'success': False, 'message': 'Los nombres nuevos no coinciden.'}) + + success_url = request.build_absolute_uri(reverse('guild-success')) + cancel_url = request.build_absolute_uri(reverse('guild-cancel')) + + stripe_response = create_checkout_session( + account_id=user_id, + username=username, + email=email, + acore_ip=acore_ip, + amount=rename_cost, + character_name=old_guild_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Renombrar hermandad: {old_guild_name} → {new_guild_name}" + ) + + if stripe_response["success"]: + request.session['guild_rename'] = { + 'old_name': old_guild_name, + 'new_name': new_guild_name + } + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'account/rename_guild.html', { + 'guild_leader_data': guild_leader_data, + 'rename_cost': rename_cost + }) +@require_paid_stripe() +def guild_rename_success_view(request): + # Obtener los datos de la sesión + guild_rename_data = request.session.get('guild_rename') + if not guild_rename_data: + return redirect('rename-cancel') # Redirigir si no hay datos de renombrado + + old_guild_name = guild_rename_data.get('old_name') + new_guild_name = guild_rename_data.get('new_name') + + if not old_guild_name or not new_guild_name: + return redirect('rename-cancel') # Redirigir si faltan datos + + # Ejecutar el comando SOAP + command = f".guild rename {old_guild_name} {new_guild_name}" + response = execute_soap_command(command) + + # Limpiar la sesión después de la ejecución + request.session.pop('guild_rename', None) + + if response is None or response.strip() == "": + message = f"La Hermandad '{old_guild_name}' ha cambiado su nombre a '{new_guild_name}'. Cierra sesión y vuelve a entrar para que los cambios se reflejen en el juego." + return render(request, 'account/guild_rename_success.html', {'message': message}) + else: + return render(request, 'account/guild_rename_success.html', {'message': f"Error: {response}"}) +def guild_rename_cancel_view(request): + """ + Vista para la página de intercambio de puntos. + """ + return render(request, 'account/guild_rename_cancel.html') diff --git a/home/views/history.py b/home/views/history.py new file mode 100644 index 0000000..2ae4e37 --- /dev/null +++ b/home/views/history.py @@ -0,0 +1,71 @@ +from ._base import * +from .account import get_account_id + + +def format_datetime(timestamp): + if timestamp: + return datetime.utcfromtimestamp(timestamp).strftime('%d-%m-%Y %H:%M:%S') + return "No disponible" + + +def get_ban_history(account_id): + # Consulta de baneos de la cuenta + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT bandate, unbandate, banreason, active, account.username + FROM account_banned + JOIN account ON account_banned.id = account.id + WHERE account_banned.id = %s + """, [account_id]) + account_bans = cursor.fetchall() + + # Formatear las fechas + account_bans = [(format_datetime(ban[0]), format_datetime(ban[1]), ban[2], ban[3], ban[4]) for ban in account_bans] + + # Consulta de baneos de los personajes asociados a la cuenta + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT bandate, unbandate, banreason, active, characters.name + FROM character_banned + JOIN characters ON character_banned.guid = characters.guid + WHERE characters.account = %s + """, [account_id]) + character_bans = cursor.fetchall() + + # Formatear las fechas + character_bans = [(format_datetime(ban[0]), format_datetime(ban[1]), ban[2], ban[3], ban[4]) for ban in character_bans] + + return account_bans, character_bans +def calculate_unmute_date(mute_date, mute_duration): + if isinstance(mute_date, int): + mute_date = datetime.fromtimestamp(mute_date) + return mute_date + timedelta(seconds=mute_duration) +def ban_history_view(request): + # Verificar si el usuario tiene un `username` en la sesión + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener el account_id a partir del username + account_id = get_account_id(username) + if not account_id: + return redirect('login') # O redirigir a otra página si no se encuentra el account_id + + print(f"Account ID: {account_id}") # Depurar el account_id + + account_bans, character_bans = get_ban_history(account_id) + + return render(request, 'account/ban_history.html', { + 'account_bans': account_bans, + 'character_bans': character_bans, + 'now': datetime.now(), + }) +def security_history_view(request): + username = request.session.get('username') + if not username: + return redirect('login') # Si no está autenticado, redirigir al login + + # Obtener los últimos 20 intentos de inicio de sesión + login_attempts = LoginAttempt.objects.filter(username=username).order_by('-timestamp')[:20] + + return render(request, 'account/security_history.html', {'login_attempts': login_attempts}) diff --git a/home/views/pages.py b/home/views/pages.py new file mode 100644 index 0000000..5f38096 --- /dev/null +++ b/home/views/pages.py @@ -0,0 +1,86 @@ +from ._base import * + + +def get_online_characters_count(): + with connections['acore_characters'].cursor() as cursor: + cursor.execute("SELECT COUNT(*) FROM characters WHERE online = 1") + result = cursor.fetchone() + return result[0] if result else 0 +def check_server_status(host, port): + if not host or host == "N/A": + return False + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + try: + s.connect((host, port)) + s.close() + return True + except (socket.timeout, ConnectionRefusedError, socket.gaierror): + return False +def home_view(request): + noticias = Noticia.objects.order_by('-fecha_publicacion')[:50] + online_characters = get_online_characters_count() + server_selection = ServerSelection.objects.first() + status = check_server_status(server_selection.address, server_selection.port) if server_selection else None + game_version = 'WotLK' if server_selection and server_selection.gamebuild == 12340 else 'Cataclysm' + + context = { + 'noticias': noticias, + 'server': server_selection, + 'status': 'Online' if status else 'Offline', + 'expansion': game_version, + 'online_characters': online_characters, + } + return render(request, 'home/home.html', context) +def download_client_view(request): + download_content = DownloadClientPage.objects.first() + categorias = ClienteCategoria.objects.all() + return render(request, 'download/download_client.html', { + 'download_content': download_content, + 'categorias': categorias, + }) +def download_addons_view(request): + return redirect("https://foro.novawow.com/files/") +def content_creators_view(request): + content_creator = ContentCreator.objects.first() + return render(request, 'community/content_creators.html', {'content_creator': content_creator}) +def server_status_view(request): + server_selection = ServerSelection.objects.first() + if server_selection: + status = check_server_status(server_selection.address, server_selection.port) + game_version = 'WotLK' if server_selection.gamebuild == 12340 else 'Cataclysm' + else: + status = None + game_version = None + + return render(request, 'server_status/server_status.html', { + 'server': server_selection, + 'status': 'Online' if status else 'Offline', + 'game_version': game_version, + }) +def novawow_realm_view(request): + return render(request, 'realms/novawow_realm.html') +def contact_us_view(request): + return render(request, 'contact/contact_us.html') +def legal_notice_view(request): + return render(request, 'legal/legal_notice.html') +def maintenance_view(request): + return render(request, 'maintenance/maintenance.html') +def not_found(request, exception=None): + return render(request, '404.html', status=404) +def terms_and_conditions_view(request): + return render(request, 'legal/terms_and_conditions.html') +def privacy_policy_view(request): + return render(request, 'legal/privacy_policy.html') +def refund_policy_view(request): + return render(request, 'legal/refund_policy.html') +def cookies_view(request): + return render(request, 'legal/cookies.html') +def help_view(request): + # Verificar si el usuario está logueado + if 'username' not in request.session: + # Si no está logueado, redirigir al inicio de sesión + return redirect('index') + + # Si está logueado, renderizar la página de ayuda + return render(request, 'community/help.html') diff --git a/home/views/payments.py b/home/views/payments.py new file mode 100644 index 0000000..3f887d5 --- /dev/null +++ b/home/views/payments.py @@ -0,0 +1,132 @@ +from ._base import * + + +@csrf_exempt +def stripe_webhook(request): + payload = request.body + sig_header = request.META.get("HTTP_STRIPE_SIGNATURE") + + try: + event = stripe.Webhook.construct_event(payload, sig_header, settings.STRIPE_WEBHOOK_SECRET) + + if event["type"] == "checkout.session.completed": + session = event["data"]["object"] + session_id = session["id"] + stripe_ip = request.META.get("REMOTE_ADDR") # IP detectada + + # 🔹 Actualizar la IP de Stripe en el log + StripeLog.objects.filter(session_id=session_id).update(stripe_ip=stripe_ip, timestamp=now()) + + return JsonResponse({"success": True}) + + except stripe.error.SignatureVerificationError: + return JsonResponse({"success": False, "message": "Firma de webhook no válida."}, status=400) +def get_pending_battlepay_orders(account_id): + """Órdenes Battlepay PENDING de una cuenta de juego.""" + with connections['acore_auth'].cursor() as cursor: + cursor.execute( + "SELECT id, reference, product_id, product_name, price_eur, status, created_at " + "FROM battlepay_orders WHERE account_id = %s AND status = 'PENDING' " + "ORDER BY created_at DESC", + [account_id], + ) + cols = [c[0] for c in cursor.description] + return [dict(zip(cols, row)) for row in cursor.fetchall()] +def _mark_order_paid_by_reference(reference): + """Marca una orden como PAID solo si estaba PENDING. Devuelve filas afectadas.""" + with connections['acore_auth'].cursor() as cursor: + cursor.execute( + "UPDATE battlepay_orders SET status = 'PAID', paid_at = UNIX_TIMESTAMP() " + "WHERE reference = %s AND status = 'PENDING'", + [reference], + ) + return cursor.rowcount +def battlepay_view(request): + """Lista las órdenes Battlepay pendientes de la cuenta de juego seleccionada.""" + account_id = request.session.get('account_id') + if not account_id: + if request.session.get('bnet_id'): + return redirect('select-account') + return redirect('login') + + orders = get_pending_battlepay_orders(account_id) + return render(request, 'account/battlepay.html', { + 'orders': orders, + 'username': request.session.get('username'), + }) +def battlepay_pay_view(request): + """Crea un checkout de SumUp para pagar una orden Battlepay pendiente.""" + account_id = request.session.get('account_id') + if not account_id: + return redirect('login') + if request.method != 'POST': + return redirect('battlepay') + + reference = request.POST.get('reference', '').strip() + + # Validar que la orden pertenece a la cuenta y sigue pendiente + with connections['acore_auth'].cursor() as cursor: + cursor.execute( + "SELECT product_name, price_eur FROM battlepay_orders " + "WHERE reference = %s AND account_id = %s AND status = 'PENDING'", + [reference, account_id], + ) + row = cursor.fetchone() + + if not row: + return render(request, 'account/pago_sumup.html', {'error': 'Orden no encontrada o ya procesada.'}) + + product_name, price_eur = row + try: + checkout = crear_checkout_battlepay(reference, price_eur, product_name) + except Exception as e: + return render(request, 'account/pago_sumup.html', {'error': str(e)}) + + return render(request, 'account/pago_sumup.html', { + 'checkout_id': checkout.get('id'), + 'reference': reference, + 'product_name': product_name, + 'price_eur': price_eur, + }) +def pagar_sumup(request): + """Alias de compatibilidad: envía al listado de órdenes Battlepay.""" + return redirect('battlepay') +@csrf_exempt +def sumup_webhook(request): + """Webhook de SumUp: marca la orden Battlepay como PAID al completarse el pago.""" + if request.method != "POST": + return JsonResponse({"error": "Método no permitido"}, status=405) + + try: + data = json.loads(request.body or b'{}') + except json.JSONDecodeError: + return JsonResponse({"error": "JSON inválido"}, status=400) + + payload = data.get('payload') or data.get('data') or {} + reference = data.get('checkout_reference') or payload.get('checkout_reference') + status = (data.get('status') or payload.get('status') or '').upper() + checkout_id = data.get('id') or payload.get('checkout_id') or payload.get('id') + event_type = (data.get('event_type') or '').lower() + + # Si falta la referencia o el estado, consultarlo a la API de SumUp + if (not reference or status not in ('PAID', 'SUCCESSFUL')) and checkout_id: + try: + checkout = obtener_checkout(checkout_id) + reference = reference or checkout.get('checkout_reference') + status = (checkout.get('status') or status).upper() + except Exception as e: + logger.error("SumUp webhook: no se pudo consultar el checkout %s: %s", checkout_id, e) + + pagado = status in ('PAID', 'SUCCESSFUL') or event_type == 'transaction.successful' + + if reference and pagado: + try: + afectadas = _mark_order_paid_by_reference(reference) + except Exception as e: + logger.error("SumUp webhook: error actualizando la orden %s: %s", reference, e) + return JsonResponse({"error": "Error interno"}, status=500) + if afectadas: + return JsonResponse({"message": "Orden marcada como pagada"}, status=200) + return JsonResponse({"message": "Orden no pendiente o inexistente"}, status=200) + + return JsonResponse({"message": "Evento ignorado"}, status=200) diff --git a/home/views/players.py b/home/views/players.py new file mode 100644 index 0000000..683082d --- /dev/null +++ b/home/views/players.py @@ -0,0 +1,228 @@ +from ._base import * + + +def get_race_icon(race): + race_icons = { + 1: "human-male", + 2: "orc-male", + 3: "dwarf-male", + 4: "night-elf-male", + 5: "undead-male", + 6: "tauren-male", + 7: "gnome-male", + 8: "troll-male", + 9: "goblin-male", + 10: "blood-elf-male", + 11: "draenei-male" + } + return race_icons.get(race, "unknown") +def get_race_iconss(race): + race_iconss = { + 1: "human", + 2: "orc", + 3: "dwarf", + 4: "night-elf", + 5: "undead", + 6: "tauren", + 7: "gnome", + 8: "troll", + 9: "goblin", + 10: "blood-elf", + 11: "draenei" + } + return race_iconss.get(race, "unknown") +def get_class_icon(char_class): + class_icons = { + 1: "warrior", + 2: "paladin", + 3: "hunter", + 4: "rogue", + 5: "priest", + 6: "death-knight", + 7: "shaman", + 8: "mage", + 9: "warlock", + 11: "druid" + } + return class_icons.get(char_class, "unknown") +def get_class_name(char_class): + names = { + 1: "Guerrero", + 2: "Paladín", + 3: "Cazador", + 4: "Pícaro", + 5: "Sacerdote", + 6: "Caballero de la Muerte", + 7: "Chamán", + 8: "Mago", + 9: "Brujo", + 11: "Druida" + } + return names.get(char_class, "Desconocido") +def get_achievement_name(achievement_id): + achievement_names = { + 457: "¡Primero del reino! Nivel 80", + 458: "¡Primero del reino! Pícaro nivel 80", + 463: "¡Primero del reino! Brujo nivel 80", + 461: "¡Primero del reino! Caballero de la Muerte nivel 80", + 462: "¡Primero del reino! Cazador nivel 80", + 467: "¡Primero del reino! Chamán nivel 80", + 466: "¡Primero del reino! Druida nivel 80", + 459: "¡Primero del reino! Guerrero nivel 80", + 460: "¡Primero del reino! Mago nivel 80", + 465: "¡Primero del reino! Paladín nivel 80", + 464: "¡Primero del reino! Sacerdote nivel 80", + 1408: "¡Primero del reino! Humano nivel 80", + 1407: "¡Primero del reino! Enano nivel 80", + 1409: "¡Primero del reino! Elfo de la noche nivel 80", + 1404: "¡Primero del reino! Gnomo nivel 80", + 1406: "¡Primero del reino! Draenei nivel 80", + 1410: "¡Primero del reino! Orco nivel 80", + 1413: "¡Primero del reino! Renegado nivel 80", + 1411: "¡Primero del reino! Tauren nivel 80", + 1412: "¡Primero del reino! Trol nivel 80", + 1405: "¡Primero del reino! Elfo de sangre nivel 80" + } + return achievement_names.get(achievement_id, "Logro Desconocido") +def novawow_players_view(request): + # Verificar si el usuario está logueado + if 'username' not in request.session: + return redirect('index') + + # Obtener las 5 hermandades con más miembros + with connections['acore_characters'].cursor() as cursor: + guild_query = """ + SELECT g.name, c.name AS leader, + CASE WHEN c.race IN (1, 3, 4, 7, 11) THEN 'alliance' ELSE 'horde' END AS faction, + COUNT(gm.guid) AS members, + g.createdate + FROM guild g + JOIN characters c ON c.guid = g.leaderguid + LEFT JOIN guild_member gm ON gm.guildid = g.guildid + GROUP BY g.guildid + ORDER BY members DESC + LIMIT 5 + """ + cursor.execute(guild_query) + guilds = cursor.fetchall() + + formatted_guilds = [ + (name, leader, faction, members, datetime.fromtimestamp(createdate).strftime('%d-%m-%Y')) + for name, leader, faction, members, createdate in guilds + ] + + # Obtener el top 10 de jugadores con más muertes con honor + with connections['acore_characters'].cursor() as cursor: + kills_query = """ + SELECT name, race, class, totalKills, todayKills + FROM characters + ORDER BY totalKills DESC + LIMIT 10 + """ + cursor.execute(kills_query) + top_kills = cursor.fetchall() + + players_with_kills = [ + { + 'name': name, + 'race_icon': f"big-{get_race_icon(race)}", + 'class_icon': f"{get_class_icon(char_class)}-medium", + 'total_kills': total_kills, + 'today_kills': today_kills + } + for name, race, char_class, total_kills, today_kills in top_kills + ] + + # Obtener personajes por clase y facción + with connections['acore_characters'].cursor() as cursor: + class_faction_query = """ + SELECT class, + SUM(CASE WHEN race IN (1, 3, 4, 7, 11) THEN 1 ELSE 0 END) AS alliance_count, + SUM(CASE WHEN race IN (2, 5, 6, 8, 10) THEN 1 ELSE 0 END) AS horde_count, + COUNT(*) AS total_count + FROM characters + GROUP BY class + """ + cursor.execute(class_faction_query) + class_faction_data = cursor.fetchall() + + classes_data = [ + { + 'class_icon': f"{get_class_icon(char_class)}-chest", + 'class_name': get_class_name(char_class), + 'total': total_count, + 'alliance': alliance_count, + 'horde': horde_count + } + for char_class, alliance_count, horde_count, total_count in class_faction_data + ] + + # Obtener el top 10 de jugadores con más logros + with connections['acore_characters'].cursor() as cursor: + achievements_query = """ + SELECT c.name, c.race, c.class, MAX(cap.counter) AS achievement_points + FROM character_achievement_progress cap + JOIN characters c ON cap.guid = c.guid + GROUP BY c.guid + ORDER BY achievement_points DESC + LIMIT 10 + """ + cursor.execute(achievements_query) + top_achievements = cursor.fetchall() + + players_with_achievements = [ + { + 'name': name, + 'race_icon': f"big-{get_race_icon(race)}", + 'class_icon': f"{get_class_icon(char_class)}-medium", + 'achievement_points': achievement_points + } + for name, race, char_class, achievement_points in top_achievements + ] + + # Obtener logros "Primeros del Reino" + with connections['acore_characters'].cursor() as cursor: + first_realm_query = """ + SELECT ca.achievement, c.name, c.race, c.class, c.gender, ca.date + FROM character_achievement ca + JOIN characters c ON ca.guid = c.guid + WHERE ca.achievement IN ( + 457, 458, 463, 461, 462, 467, 466, 459, 460, 465, 464, + 1408, 1407, 1409, 1404, 1406, 1410, 1413, 1411, 1412, 1405 + ) + ORDER BY ca.date ASC + """ + cursor.execute(first_realm_query) + first_realm_achievements = cursor.fetchall() + + first_realm_data = [] + for achievement in first_realm_achievements: + achievement_id, name, race, char_class, gender, date = achievement + formatted_date = datetime.fromtimestamp(date).strftime('%H:%M:%S %d-%m-%Y') + + # Determinar el ícono correcto basado en el logro + if achievement_id == 457: + icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-icons/achievement-level-80.jpg" + elif achievement_id in [1408, 1407, 1409, 1404, 1406, 1410, 1413, 1411, 1412, 1405]: + race_iconss = get_race_iconss(race) + gender_suffix = "male" if gender == 0 else "female" + icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-races/big-{race_iconss}-{gender_suffix}.webp" + else: + icon_url = f"{settings.URL_PRINCIPAL}/static/nw-themes/nw-ryu/nw-images/nw-classes/{get_class_icon(char_class)}-medium.jpg" + + first_realm_data.append({ + 'achievement_name': get_achievement_name(achievement_id), + 'name': name, + 'race_iconss': f"big-{get_race_iconss(race)}", + 'icon_url': icon_url, + 'date': formatted_date + }) + + # Renderizar la plantilla con los datos obtenidos + return render(request, 'realms/novawow_players.html', { + 'guilds': formatted_guilds, + 'players_with_kills': players_with_kills, + 'players_with_achievements': players_with_achievements, + 'classes_data': classes_data, + 'first_realm_data': first_realm_data + }) diff --git a/home/views/points.py b/home/views/points.py new file mode 100644 index 0000000..ff6265b --- /dev/null +++ b/home/views/points.py @@ -0,0 +1,89 @@ +from ._base import * + + +def promo_code_view(request): + return render(request, 'account/promo_code.html') +def transfer_d_points_view(request): + # Renderizar la plantilla para la transferencia de puntos + return render(request, 'account/transfer_d_points.html') +def vote_points_view(request): + # Verificar si el usuario tiene un `username` en la sesión + username = request.session.get('username') + if not username: + return redirect('login') + + # Obtener el `account_id` basado en el `username` + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id FROM account WHERE username = %s", [username]) + account_data = cursor.fetchone() + + if not account_data: + return JsonResponse({'success': False, 'message': 'Cuenta no encontrada.'}) + + account_id = account_data[0] + + if request.method == 'POST': + vote_url = request.POST.get('vote').strip() + + site = VoteSite.objects.filter(url=vote_url).first() + if not site: + return JsonResponse({'success': False, 'message': 'Sitio de votación no encontrado.'}) + + # Verificar si ya ha votado en las últimas 12 horas y 30 minutos + last_vote = VoteLog.objects.filter(account_id=account_id, vote_site=site).order_by('-created_at').first() + now = timezone.now() + if last_vote: + time_diff = now - last_vote.created_at + cooldown_period = timedelta(hours=12, minutes=30) + + if time_diff < cooldown_period: + remaining_time = cooldown_period - time_diff + hours, remainder = divmod(remaining_time.seconds, 3600) + minutes, _ = divmod(remainder, 60) + return JsonResponse({ + 'success': False, + 'message': f'Tienes que esperar {hours} horas y {minutes} minutos antes de volver a votar.' + }) + + # Acreditar puntos al usuario y registrar el voto + user_points = HomeApiPoints.objects.filter(accountID=account_id).first() + if user_points: + user_points.vp += site.points + user_points.save() + else: + HomeApiPoints.objects.create(accountID=account_id, vp=site.points, dp=0) + + # Registrar el voto + VoteLog.objects.create(account_id=account_id, vote_site=site, last_vote_time=now) + + return JsonResponse({'success': True, 'message': f'Has votado en {site.name}. Se han acreditado {site.points} PV.'}) + + vote_sites = VoteSite.objects.all() + return render(request, 'account/vote_points.html', {'vote_sites': vote_sites}) +def d_points_view(request): + """ + Vista para la página de D-Points. + Simplemente renderiza la plantilla d_points.html. + """ + return render(request, 'account/d_points.html') +def points_history_view(request): + return render(request, 'account/points_history.html') +def trans_history_view(request): + """ + Vista para mostrar el historial de transacciones de la cuenta y sus personajes. + """ + username = request.session.get('username') # Obtener el nombre de usuario de la sesión + + if username: + # Filtramos las transacciones que corresponden al usuario y a los personajes asociados + transactions = StripeLog.objects.filter(username=username) + else: + transactions = [] + + # Renderizamos la vista con el historial de transacciones + return render(request, 'account/trans_history.html', {'transactions': transactions}) +def trade_points_view(request): + """ + Vista para la página de intercambio de puntos. + """ + return render(request, 'account/trade_points.html') diff --git a/home/views/recruit.py b/home/views/recruit.py new file mode 100644 index 0000000..f4ec1b3 --- /dev/null +++ b/home/views/recruit.py @@ -0,0 +1,161 @@ +from ._base import * +from .account import get_account_status, get_account_characters + + +def recruit_a_friend_view(request): + username = request.session.get('username') + account_status = None + characters = [] + claimed_rewards_count = 0 + + # Si el usuario está logueado, obtener su información + if username: + # Obtener información del usuario + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id, last_ip FROM account WHERE username = %s", [username]) + account_data = cursor.fetchone() + + if account_data: + account_id = account_data[0] + user_ip = account_data[1] + account_status = get_account_status(account_id) + characters = get_account_characters(account_id) + claimed_rewards_count = ClaimedReward.objects.filter(account_id=account_id).count() + else: + account_id = None + user_ip = None + else: + account_id = None + user_ip = None + + # Obtener todas las recompensas + all_rewards = RecruitReward.objects.all() + available_rewards = [] + + # Si el usuario está logueado, filtrar recompensas disponibles + if account_id: + available_rewards = [reward for reward in all_rewards if not ClaimedReward.objects.filter(account_id=account_id, recruit_reward=reward).exists()] + + # Manejar solicitudes POST (solo si el usuario está logueado) + if request.method == 'POST': + if not username: + return JsonResponse({'success': False, 'message': 'Debes estar logueado para reclamar una recompensa.'}) + + if not account_id or account_status is None: + return JsonResponse({'success': False, 'message': 'No se ha encontrado tu cuenta.'}) + + reward_id = request.POST.get('reward_id') + character_name = request.POST.get('character') + + if not reward_id or not character_name: + return JsonResponse({'success': False, 'message': 'Selecciona una recompensa y un personaje.'}) + + try: + reward = RecruitReward.objects.get(id=reward_id) + + # Verificar si ya se ha reclamado la recompensa + if ClaimedReward.objects.filter(account_id=account_id, recruit_reward=reward).exists(): + return JsonResponse({'success': False, 'message': 'Ya has reclamado esta recompensa.'}) + + # Verificar si cumple los requisitos + if account_status['recruited_level_80_count'] < reward.required_friends: + return JsonResponse({'success': False, 'message': 'No cumples con los requisitos.'}) + + # Obtener amigos reclutados que han alcanzado el nivel 80 + recruited_friends = get_recruited_friends_at_level_80(account_id, reward.required_friends) + + # Comprobar si alguno de los reclutados es válido + if len(recruited_friends) < reward.required_friends: + return JsonResponse({'success': False, 'message': 'No cumples con los requisitos o los amigos tienen la misma IP.'}) + + # Comprobar si alguno de los reclutados tiene la misma IP que el usuario + if check_recruited_ip_conflict(recruited_friends, user_ip): + return JsonResponse({'success': False, 'message': 'No puedes reclamar recompensas de amigos que tienen la misma IP.'}) + + # Ejecutar el comando SOAP + command = f".send items {character_name} 'Recompensa de Recluta Amigo' 'Tu amigo alcanzó el Nivel 80' {reward.item_id}:{reward.item_quantity}" + response = execute_soap_command(command) + + if response and "Mail sent to" in response: + # Registrar la recompensa como reclamada + ClaimedReward.objects.create( + account_id=account_id, + username=username, + recruit_reward=reward, + character_name=character_name, + claimed_at=timezone.now(), + ip_address=request.META.get('REMOTE_ADDR') + ) + claimed_rewards_count = ClaimedReward.objects.filter(account_id=account_id).count() + + return JsonResponse({ + 'success': True, + 'message': f"Recompensa '{reward.reward_name}' entregada a {character_name}.", + 'claimed_count': claimed_rewards_count + }) + else: + return JsonResponse({'success': False, 'message': 'Error al entregar la recompensa.'}) + + except RecruitReward.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Recompensa no encontrada.'}) + + # Renderizar la página para usuarios no logueados y logueados + return render(request, 'recruit/recruit_a_friend.html', { + 'account_status': account_status, + 'all_rewards': all_rewards, + 'available_rewards': available_rewards, + 'characters': characters, + 'claimed_rewards_count': claimed_rewards_count, + 'is_logged_in': username is not None + }) +def check_recruited_ip_conflict(recruited_accounts, user_ip): + """ + Verifica si alguna de las cuentas reclutadas tiene la misma IP que la cuenta del reclutador. + """ + if not recruited_accounts: + return False + + with connections['acore_auth'].cursor() as cursor: + cursor.execute(""" + SELECT last_ip FROM account WHERE id IN %s + """, [tuple(recruited_accounts)]) + recruited_ips = [ip[0] for ip in cursor.fetchall()] + + return user_ip in recruited_ips +def get_recruited_friends_at_level_80(account_id, required_count): + """ + Obtiene una lista de IDs de amigos reclutados que han alcanzado el nivel 80 y + no tienen la misma IP que el reclutador. + """ + # Primero obtener los IDs de cuentas reclutadas desde `acore_auth` + with connections['acore_auth'].cursor() as auth_cursor: + auth_cursor.execute(""" + SELECT id, last_ip FROM account WHERE recruiter = %s + """, [account_id]) + recruited_accounts = auth_cursor.fetchall() + + if not recruited_accounts: + return [] + + # Extraer IDs y IPs de las cuentas reclutadas + recruited_ids = [acc[0] for acc in recruited_accounts] + recruited_ips = {acc[0]: acc[1] for acc in recruited_accounts} + + # Obtener la IP del usuario actual + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT last_ip FROM account WHERE id = %s", [account_id]) + user_ip = cursor.fetchone()[0] + + # Luego, verificar cuáles de esas cuentas tienen personajes al nivel 80 en `acore_characters` + with connections['acore_characters'].cursor() as char_cursor: + char_cursor.execute(""" + SELECT DISTINCT account FROM characters + WHERE level = 80 AND account IN %s + LIMIT %s + """, [tuple(recruited_ids), required_count]) + recruited_at_level_80 = [acc[0] for acc in char_cursor.fetchall()] + + # Filtrar cuentas que no tienen la misma IP que el usuario actual + valid_recruits = [acc for acc in recruited_at_level_80 if recruited_ips[acc] != user_ip] + + return valid_recruits diff --git a/home/views/store.py b/home/views/store.py new file mode 100644 index 0000000..7449dbe --- /dev/null +++ b/home/views/store.py @@ -0,0 +1,173 @@ +from ._base import * + + +def store_novawow_cancel_view(request): + return render(request, 'store/store_novawow_cancel.html') +def store_novawow_view(request): + username = request.session.get('username') + if not username: + return redirect('login') + + category_name = request.GET.get("category") + + # Obtener personajes del usuario desde la base de datos acore_characters + with connections['acore_characters'].cursor() as cursor: + cursor.execute(""" + SELECT name + FROM characters + WHERE account = ( + SELECT id + FROM acore_auth.account + WHERE username = %s + ) + """, [username]) + characters = [row[0] for row in cursor.fetchall()] + + # Obtener categorías e ítems + categories = Category.objects.prefetch_related("items").all() + store_data = {category.name: list(category.items.values()) for category in categories} + + # Si se está solicitando una categoría específica, devolver solo los ítems de esa categoría + if category_name: + try: + category = Category.objects.get(name=category_name) + except Category.DoesNotExist: + return JsonResponse({'success': False, 'message': 'Categoría no encontrada'}, status=404) + return JsonResponse({'success': True, 'items': list(category.items.values())}) + + # Obtener carrito desde la sesión + carrito = request.session.get('carrito', []) + + # Calcular el precio total con 2 decimales + total_price = sum(Decimal(item['precio']) for item in carrito) + total_price_with_iva = (total_price * Decimal('1.21')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) + + if request.method == 'POST': + # Obtener datos del body + data = json.loads(request.body) + character_name = data.get('character') + carrito_cliente = data.get('carrito') or [] + + if not character_name: + return JsonResponse({'success': False, 'message': 'Por favor, selecciona un personaje.'}) + + # Verificar si el personaje pertenece al usuario + if character_name not in characters: + return JsonResponse({'success': False, 'message': 'No tienes permiso para este personaje.'}) + + if not carrito_cliente: + return JsonResponse({'success': False, 'message': 'El carrito está vacío.'}) + + # VALIDAR ÍTEMS Y PRECIOS CONTRA LA BASE DE DATOS (nunca confiar en el + # precio ni en los ítems que manda el cliente). + try: + ids_solicitados = [int(item['id']) for item in carrito_cliente] + except (KeyError, TypeError, ValueError): + return JsonResponse({'success': False, 'message': 'Carrito no válido.'}) + + items_db = {i.item_id: i for i in Item.objects.filter(item_id__in=ids_solicitados)} + + carrito = [] # carrito saneado que se guardará en sesión + total_price = Decimal('0.00') + for item in carrito_cliente: + item_id = int(item['id']) + db_item = items_db.get(item_id) + if not db_item: + return JsonResponse({'success': False, 'message': f'Ítem no disponible en la tienda (ID {item_id}).'}) + try: + cantidad = max(1, int(item.get('cantidad', 1))) + except (TypeError, ValueError): + cantidad = 1 + total_price += db_item.price * cantidad + carrito.append({'id': item_id, 'nombre': db_item.name, 'cantidad': cantidad}) + + total_price_with_iva = (total_price * Decimal('1.21')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) + + # Guardar en sesión el carrito YA VALIDADO (para la entrega tras el pago) + request.session['carrito'] = carrito + request.session.modified = True + + # Descripción del producto a partir de los datos validados + product_description = ", ".join([f"{it['nombre']} x{it['cantidad']}" for it in carrito]) + + # Obtener detalles del usuario + with connections['acore_auth'].cursor() as cursor: + cursor.execute("SELECT id, email, last_ip FROM account WHERE username = %s", [username]) + user_data = cursor.fetchone() + + if not user_data: + return JsonResponse({'success': False, 'message': 'Cuenta no encontrada.'}) + + user_id, email, acore_ip = user_data # ID de la cuenta, email y la última IP + + # Crear sesión de pago con Stripe + success_url = request.build_absolute_uri(reverse('store-novawow/success')) + cancel_url = request.build_absolute_uri(reverse('store-novawow/cancel')) + + # Llamada corregida a create_checkout_session con todos los parámetros requeridos + stripe_response = create_checkout_session( + username=username, + email=email, + acore_ip=acore_ip, + account_id=user_id, # Pasar el account_id + amount=total_price_with_iva, # El precio con IVA + character_name=character_name, + success_url=success_url, + cancel_url=cancel_url, + product_name=f"Compra de ítems para el personaje: {character_name} : {product_description}" + ) + + if stripe_response["success"]: + request.session['store_novawow'] = character_name + return JsonResponse({ + 'success': True, + 'session_id': stripe_response["session_id"], + 'stripe_public_key': settings.STRIPE_PUBLIC_KEY + }) + else: + return JsonResponse({'success': False, 'message': f'Error al iniciar el pago: {stripe_response["error"]}'}) + + return render(request, 'store/store_novawow.html', { + 'characters': characters, + 'store_data': store_data, + 'categories': categories, + 'total_price': total_price_with_iva + }) +@require_paid_stripe(redirect_to='store-novawow') +def store_novawow_success_view(request): + # Procesar el comando SOAP tras el pago exitoso + character_name = request.session.get('store_novawow') + carrito = request.session.get('carrito', []) + + if not character_name or not carrito: + return redirect('store-novawow') + + # Contar correctamente los ítems por ID + item_counts = Counter(item["id"] for item in carrito) + + # Generar la lista de ítems en formato "ID:Cantidad" + items_str = " ".join(f"{item_id}:{count}" for item_id, count in item_counts.items()) + + # Construir el comando SOAP + command = f".send items {character_name} 'Gracias por donar a Nova WoW' 'Aquí tienes tu donación' {items_str}" + + # Ejecutar el comando SOAP + response = execute_soap_command(command) + + # Limpiar la sesión después del envío + request.session.pop('store_novawow', None) + request.session.pop('carrito', None) + + if response is None or response.strip() == "": + message = f"Los ítems han sido añadidos al personaje {character_name} correctamente." + return render(request, 'store/store_novawow_success.html', {'message': message}) + else: + return render(request, 'store/store_novawow_success.html', {'message': f"Error: {response}"}) +def store_novawow_buscar_categoria_view(request, categoria): + try: + # Buscar la categoría desde el parámetro de la URL + category = Category.objects.get(name=categoria) + items = category.items.all().values("id", "name", "price", "image_url") + return JsonResponse({"success": True, "items": list(items)}) + except Category.DoesNotExist: + return JsonResponse({"success": False, "message": "Categoría no encontrada"}, status=404)