Improve insights and continue WhatsApp implementation

This commit is contained in:
2026-02-15 23:02:51 +00:00
parent b23af9bc7f
commit 88224d972c
13 changed files with 628 additions and 81 deletions

View File

@@ -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"))

View File

@@ -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(),
}