import hashlib import logging from django.contrib.auth import get_user_model from django.db import transaction from rest_framework.authtoken.models import Token from utils.ip_helper import get_client_ip logger = logging.getLogger(__name__) User = get_user_model() def _extract_token_from_auth_header(request) -> str: auth_header = (request.META.get('HTTP_AUTHORIZATION') or '').strip() if not auth_header: return '' parts = auth_header.split() if len(parts) == 2 and parts[0].lower() in ('token', 'bearer'): return parts[1].strip() return '' def compute_web_guest_device_id(request) -> str: x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: client_ip = x_forwarded_for.split(',')[0] else: client_ip = request.META.get('REMOTE_ADDR') or get_client_ip(request) or '' user_agent = request.META.get('HTTP_USER_AGENT', '') ua_hash = hashlib.md5(user_agent.encode('utf-8')).hexdigest()[:8] return f"{client_ip}_{ua_hash}" def resolve_guest_users_from_request(request, device_id: str = None) -> list: """ Finds any Guest User(s) (`email__isnull=True`) associated with the current request via X-Guest-Token header, Authorization token header, explicit device_id, or Web IP+UA device_id. Returns a deduplicated list of guest User instances in priority order. """ candidates = [] seen_ids = set() def _add(u): if u and getattr(u, 'email', None) is None and u.id not in seen_ids: seen_ids.add(u.id) candidates.append(u) # 1. Explicit X-Guest-Token header x_guest_key = (request.headers.get('X-Guest-Token') or '').strip() if x_guest_key: token_obj = Token.objects.select_related('user').filter(key=x_guest_key).first() if token_obj: _add(token_obj.user) # 2. Authorization: token header (sent automatically by frontend before auth_token is set) auth_key = _extract_token_from_auth_header(request) if auth_key and auth_key != x_guest_key: token_obj = Token.objects.select_related('user').filter(key=auth_key).first() if token_obj: _add(token_obj.user) # 3. Explicit device_id parameter if device_id: guest_by_device = User.objects.filter(device_id=device_id, email__isnull=True).first() _add(guest_by_device) # 4. Web IP + User-Agent hash fallback (matches WebUserGuestView) try: web_device_id = compute_web_guest_device_id(request) if web_device_id and web_device_id != device_id: guest_by_web_id = User.objects.filter(device_id=web_device_id, email__isnull=True).first() _add(guest_by_web_id) except Exception as exc: logger.debug("Could not compute web guest device_id fallback: %s", exc) return candidates @transaction.atomic def merge_guest_user_data(guest_user, target_user) -> None: """ Merges all guest activity (SupportRoom, SupportMessage, Bookmark, Rate) from `guest_user` into `target_user`. Safe to call multiple times (idempotent). """ if not guest_user or not target_user or guest_user.id == target_user.id: return # 1. Merge Support Chat (SupportRoom & SupportMessage) try: from apps.support.models import SupportRoom, SupportMessage guest_room = SupportRoom.objects.select_for_update().filter(user=guest_user).first() if guest_room: target_room = SupportRoom.objects.select_for_update().filter(user=target_user).first() if not target_room: guest_room.user = target_user guest_room.save(update_fields=['user', 'updated_at']) SupportMessage.objects.filter(room=guest_room, sender=guest_user).update(sender=target_user) else: # Both guest and target user already have support rooms: merge messages into target_room SupportMessage.objects.filter(room=guest_room).update(room=target_room) SupportMessage.objects.filter(room=target_room, sender=guest_user).update(sender=target_user) target_room.unread_by_admin = (target_room.unread_by_admin or 0) + (guest_room.unread_by_admin or 0) target_room.unread_by_user = (target_room.unread_by_user or 0) + (guest_room.unread_by_user or 0) last_msg = target_room.messages.order_by('-id').first() if last_msg: target_room.last_message_at = last_msg.created_at if last_msg.sender_type == SupportMessage.SenderType.USER: target_room.status = SupportRoom.StatusChoices.PENDING else: target_room.status = SupportRoom.StatusChoices.ANSWERED target_room.save( update_fields=[ 'unread_by_admin', 'unread_by_user', 'last_message_at', 'status', 'updated_at', ] ) guest_room.delete() except Exception as exc: logger.error("Error merging support chat from guest %s to user %s: %s", guest_user.id, target_user.id, exc) # 2. Merge Bookmarks try: from apps.bookmark.models import Bookmark for bm in Bookmark.objects.filter(user=guest_user): existing = Bookmark.objects.filter( user=target_user, service=bm.service, content_id=bm.content_id, ).first() if existing: if bm.status and not existing.status: existing.status = True existing.save(update_fields=['status', 'updated_at']) bm.delete() else: bm.user = target_user bm.save(update_fields=['user', 'updated_at']) except Exception as exc: logger.error("Error merging bookmarks from guest %s to user %s: %s", guest_user.id, target_user.id, exc) # 3. Merge Rates try: from apps.bookmark.models import Rate for rt in Rate.objects.filter(user=guest_user): existing = Rate.objects.filter( user=target_user, service=rt.service, content_id=rt.content_id, ).first() if existing: rt.delete() else: rt.user = target_user rt.save(update_fields=['user', 'updated_at']) except Exception as exc: logger.error("Error merging rates from guest %s to user %s: %s", guest_user.id, target_user.id, exc)