"""Routes available only to staff, including isolated message attachment delivery.""" import mimetypes from django.conf import settings from django.contrib import messages from django.db import transaction from django.utils import timezone from django.core.exceptions import PermissionDenied, ValidationError from django.http import FileResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.views.decorators.cache import never_cache from django.views.decorators.http import require_GET, require_POST from apps.attachments.delivery import signed_media_redirect from apps.attachments.models import Attachment from .models import MessageShare, Ticket, TicketReadState from .policies import TicketPolicy from apps.messaging.read_receipts import message_read_ids from .staff_conversation import received_messages, unread_shares, visible_messages from .sharing import mark_share_opened, revoke_staff_share from .views import staff_required, mark_messages_read def received_message_share(request, share_id): grant = get_object_or_404(MessageShare.objects.select_related("message__author__staff_profile", "granted_by__staff_profile"), public_id=share_id) profile = getattr(request.user, "staff_profile", None) if (grant.user_id != request.user.pk or grant.revoked_at or grant.message.deleted_at or not request.user.is_active or not profile or not profile.is_active): raise PermissionDenied return grant def submit_share_form(request, form, save, success_message): if not form.is_valid(): messages.error(request, "گیرنده، سطح دسترسی یا توضیح اشتراک معتبر نیست.") return try: save() except ValidationError as error: messages.error(request, " ".join(error.messages)) else: messages.success(request, success_message) @staff_required @require_GET @never_cache def inbox(request): from django.urls import reverse return redirect(reverse("staff_tickets:list") + "?tab=shared") @staff_required @require_POST def revoke_message(request, public_id, share_id): grant = get_object_or_404(MessageShare.objects.select_related("message__ticket"), public_id=share_id, message__ticket__public_id=public_id) revoke_staff_share(grant, request.user) messages.success(request, "دسترسی به پیام اشتراکی لغو شد؛ سابقه آن حفظ شده است.") return redirect("staff_tickets:detail", public_id=public_id) @staff_required @require_GET @never_cache def shared_message(request, share_id): grant = received_message_share(request, share_id) return redirect("staff_tickets:detail", public_id=grant.message.ticket.public_id) @staff_required @require_POST @never_cache @transaction.atomic def read_shared_messages(request, public_id): ticket = get_object_or_404(Ticket.objects.select_for_update(), public_id=public_id) allowed = visible_messages(ticket, request.user) try: ids = message_read_ids(request) except ValidationError: return JsonResponse({"ok": False, "error": "invalid_message_ids"}, status=400) acknowledged = list(allowed.filter(public_id__in=ids, deleted_at__isnull=True)) mark_messages_read(acknowledged, request.user) accepted = [item.public_id for item in acknowledged] grants = received_messages(request.user, ticket).filter(message__public_id__in=accepted) for grant in grants: if grant.first_viewed_at is None: mark_share_opened(grant, request.user) latest = allowed.filter(deleted_at__isnull=True).order_by('-created_at', '-pk').first() if latest and latest.public_id in accepted and TicketPolicy.can_view(request.user, ticket): TicketReadState.objects.update_or_create(ticket=ticket, user=request.user, defaults={'last_read_at': timezone.now(), 'last_read_message': latest}) return JsonResponse({"unread_shares": unread_shares(request.user), "read_ids": [str(value) for value in accepted]}) @staff_required @require_GET @never_cache def unread_count(request): return JsonResponse({"unread_shares": unread_shares(request.user)}) @staff_required @require_GET @never_cache def shared_attachment(request, share_id, attachment_id, download=False): grant = received_message_share(request, share_id) attachment = get_object_or_404(Attachment, public_id=attachment_id, message_id=grant.message_id) if settings.USE_NGINX_MEDIA: return signed_media_redirect(attachment.relative_path, download=download) content_type = attachment.mime_type or mimetypes.guess_type(attachment.original_name)[0] or "application/octet-stream" response = FileResponse(attachment.file.open("rb"), content_type=content_type, as_attachment=download, filename=attachment.original_name) response["X-Content-Type-Options"] = "nosniff" return response