Improve insights and continue WhatsApp implementation
This commit is contained in:
@@ -5,13 +5,19 @@ import json
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timezone as dt_timezone
|
||||
from urllib.parse import urlencode, urlparse
|
||||
from urllib.parse import quote_plus, urlencode, urlparse
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core import signing
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.http import (
|
||||
HttpResponse,
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseNotFound,
|
||||
JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils import timezone as dj_timezone
|
||||
@@ -19,6 +25,7 @@ from django.views import View
|
||||
|
||||
from core.clients import transport
|
||||
from core.messaging import ai as ai_runner
|
||||
from core.messaging import media_bridge
|
||||
from core.messaging.utils import messages_to_string
|
||||
from core.models import (
|
||||
AI,
|
||||
@@ -127,9 +134,31 @@ def _looks_like_image_url(url_value: str) -> bool:
|
||||
return False
|
||||
parsed = urlparse(url_value)
|
||||
path = str(parsed.path or "").lower()
|
||||
if path.endswith("/compose/media/blob/"):
|
||||
return True
|
||||
return path.endswith(IMAGE_EXTENSIONS)
|
||||
|
||||
|
||||
def _is_xmpp_share_url(url_value: str) -> bool:
|
||||
if not url_value:
|
||||
return False
|
||||
parsed = urlparse(url_value)
|
||||
host = str(parsed.netloc or "").strip().lower()
|
||||
configured = str(
|
||||
getattr(settings, "XMPP_UPLOAD_SERVICE", "")
|
||||
or getattr(settings, "XMPP_UPLOAD_JID", "")
|
||||
).strip().lower()
|
||||
if not configured:
|
||||
return False
|
||||
configured_host = configured
|
||||
if "://" in configured:
|
||||
configured_host = (urlparse(configured).netloc or configured_host).lower()
|
||||
if "@" in configured_host:
|
||||
configured_host = configured_host.split("@", 1)[-1]
|
||||
configured_host = configured_host.split("/", 1)[0]
|
||||
return host == configured_host
|
||||
|
||||
|
||||
def _image_url_from_text(text_value: str) -> str:
|
||||
urls = _image_urls_from_text(text_value)
|
||||
return urls[0] if urls else ""
|
||||
@@ -175,12 +204,23 @@ def _extract_attachment_image_urls(blob) -> list[str]:
|
||||
filename = str(blob.get("filename") or blob.get("fileName") or "").strip()
|
||||
image_hint = content_type.startswith("image/") or _looks_like_image_name(filename)
|
||||
|
||||
direct_urls = []
|
||||
for key in ("url", "source_url", "download_url", "proxy_url", "href", "uri"):
|
||||
normalized = _clean_url(blob.get(key))
|
||||
if not normalized:
|
||||
continue
|
||||
if image_hint or _looks_like_image_url(normalized):
|
||||
urls.append(normalized)
|
||||
if (
|
||||
image_hint
|
||||
or _looks_like_image_url(normalized)
|
||||
or _is_xmpp_share_url(normalized)
|
||||
):
|
||||
direct_urls.append(normalized)
|
||||
urls.extend(direct_urls)
|
||||
blob_key = str(blob.get("blob_key") or "").strip()
|
||||
# Prefer source-hosted URLs (for example share.zm.is) and use blob fallback only
|
||||
# when no usable direct URL exists.
|
||||
if blob_key and image_hint and not direct_urls:
|
||||
urls.append(f"/compose/media/blob/?key={quote_plus(blob_key)}")
|
||||
|
||||
nested = blob.get("attachments")
|
||||
if isinstance(nested, list):
|
||||
@@ -1632,6 +1672,29 @@ class ComposeThread(LoginRequiredMixin, View):
|
||||
return JsonResponse(payload)
|
||||
|
||||
|
||||
class ComposeMediaBlob(LoginRequiredMixin, View):
|
||||
"""
|
||||
Serve cached media blobs for authenticated compose image previews.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
blob_key = str(request.GET.get("key") or "").strip()
|
||||
if not blob_key:
|
||||
return HttpResponseBadRequest("Missing blob key.")
|
||||
|
||||
row = media_bridge.get_blob(blob_key)
|
||||
if not row:
|
||||
return HttpResponseNotFound("Blob not found.")
|
||||
|
||||
content = row.get("content") or b""
|
||||
content_type = str(row.get("content_type") or "application/octet-stream")
|
||||
filename = str(row.get("filename") or "attachment.bin")
|
||||
response = HttpResponse(content, content_type=content_type)
|
||||
response["Content-Length"] = str(len(content))
|
||||
response["Content-Disposition"] = f'inline; filename="{filename}"'
|
||||
return response
|
||||
|
||||
|
||||
class ComposeDrafts(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
service = _default_service(request.GET.get("service"))
|
||||
|
||||
@@ -5,6 +5,7 @@ from mixins.views import ObjectList, ObjectRead
|
||||
|
||||
from core.clients import transport
|
||||
from core.views.manage.permissions import SuperUserRequiredMixin
|
||||
import time
|
||||
|
||||
|
||||
class WhatsApp(SuperUserRequiredMixin, View):
|
||||
@@ -88,6 +89,41 @@ class WhatsAppAccountAdd(SuperUserRequiredMixin, ObjectRead):
|
||||
"detail_url": reverse(self.detail_url_name, kwargs=detail_url_args),
|
||||
}
|
||||
|
||||
def _debug_state(self):
|
||||
state = transport.get_runtime_state(self.service)
|
||||
now = int(time.time())
|
||||
|
||||
def _age(key: str) -> str:
|
||||
try:
|
||||
value = int(state.get(key) or 0)
|
||||
except Exception:
|
||||
value = 0
|
||||
if value <= 0:
|
||||
return "n/a"
|
||||
return f"{max(0, now - value)}s ago"
|
||||
|
||||
qr_value = str(state.get("pair_qr") or "")
|
||||
return [
|
||||
f"connected={bool(state.get('connected'))}",
|
||||
f"runtime_updated={_age('updated_at')}",
|
||||
f"runtime_seen={_age('runtime_seen_at')}",
|
||||
f"pair_requested={_age('pair_requested_at')}",
|
||||
f"qr_received={_age('qr_received_at')}",
|
||||
f"last_qr_probe={_age('last_qr_probe_at')}",
|
||||
f"pair_status={state.get('pair_status') or '-'}",
|
||||
f"pair_request_source={state.get('pair_request_source') or '-'}",
|
||||
f"qr_probe_result={state.get('qr_probe_result') or '-'}",
|
||||
f"qr_handler_supported={state.get('qr_handler_supported')}",
|
||||
f"qr_handler_registered={state.get('qr_handler_registered')}",
|
||||
f"event_hook_callable={state.get('event_hook_callable')}",
|
||||
f"event_support={state.get('event_support') or {}}",
|
||||
f"last_event={state.get('last_event') or '-'}",
|
||||
f"last_error={state.get('last_error') or '-'}",
|
||||
f"pair_qr_present={bool(qr_value)} len={len(qr_value)}",
|
||||
f"accounts={state.get('accounts') or []}",
|
||||
f"warning={state.get('warning') or '-'}",
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
if self._refresh_only() and request.htmx:
|
||||
@@ -109,6 +145,7 @@ class WhatsAppAccountAdd(SuperUserRequiredMixin, ObjectRead):
|
||||
"ok": True,
|
||||
"image_b64": transport.image_bytes_to_base64(image_bytes),
|
||||
"warning": transport.get_service_warning(self.service),
|
||||
"debug_lines": self._debug_state(),
|
||||
}
|
||||
except Exception as exc:
|
||||
error_text = str(exc)
|
||||
@@ -118,4 +155,5 @@ class WhatsAppAccountAdd(SuperUserRequiredMixin, ObjectRead):
|
||||
"device": device_name,
|
||||
"error": error_text,
|
||||
"warning": transport.get_service_warning(self.service),
|
||||
"debug_lines": self._debug_state(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user