Implement executing tasks
This commit is contained in:
147
core/views/availability.py
Normal file
147
core/views/availability.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
|
||||
from core.models import (
|
||||
ContactAvailabilityEvent,
|
||||
ContactAvailabilitySettings,
|
||||
ContactAvailabilitySpan,
|
||||
Person,
|
||||
)
|
||||
|
||||
|
||||
def _to_int(value, default=0):
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
return int(default)
|
||||
|
||||
|
||||
def _to_bool(value, default=False):
|
||||
if value is None:
|
||||
return bool(default)
|
||||
text = str(value).strip().lower()
|
||||
if text in {"1", "true", "yes", "on", "y"}:
|
||||
return True
|
||||
if text in {"0", "false", "no", "off", "n"}:
|
||||
return False
|
||||
return bool(default)
|
||||
|
||||
|
||||
def _iso_to_ms(value: str) -> int:
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return 0
|
||||
try:
|
||||
dt = datetime.fromisoformat(raw)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return int(dt.timestamp() * 1000)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
class AvailabilitySettingsPage(LoginRequiredMixin, View):
|
||||
template_name = "pages/availability-settings.html"
|
||||
|
||||
def _settings(self, request):
|
||||
row, _ = ContactAvailabilitySettings.objects.get_or_create(user=request.user)
|
||||
return row
|
||||
|
||||
def post(self, request):
|
||||
row = self._settings(request)
|
||||
row.enabled = _to_bool(request.POST.get("enabled"), row.enabled)
|
||||
row.show_in_chat = _to_bool(request.POST.get("show_in_chat"), row.show_in_chat)
|
||||
row.show_in_groups = _to_bool(
|
||||
request.POST.get("show_in_groups"), row.show_in_groups
|
||||
)
|
||||
row.inference_enabled = _to_bool(
|
||||
request.POST.get("inference_enabled"), row.inference_enabled
|
||||
)
|
||||
row.retention_days = max(1, _to_int(request.POST.get("retention_days"), 90))
|
||||
row.fade_threshold_seconds = max(
|
||||
30, _to_int(request.POST.get("fade_threshold_seconds"), 900)
|
||||
)
|
||||
row.save(
|
||||
update_fields=[
|
||||
"enabled",
|
||||
"show_in_chat",
|
||||
"show_in_groups",
|
||||
"inference_enabled",
|
||||
"retention_days",
|
||||
"fade_threshold_seconds",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return self.get(request)
|
||||
|
||||
def get(self, request):
|
||||
settings_row = self._settings(request)
|
||||
person_id = str(request.GET.get("person") or "").strip()
|
||||
service = str(request.GET.get("service") or "").strip().lower()
|
||||
state = str(request.GET.get("state") or "").strip().lower()
|
||||
source_kind = str(request.GET.get("source_kind") or "").strip().lower()
|
||||
start_ts = _iso_to_ms(request.GET.get("start"))
|
||||
end_ts = _iso_to_ms(request.GET.get("end"))
|
||||
if end_ts <= 0:
|
||||
end_ts = int(datetime.now(tz=timezone.utc).timestamp() * 1000)
|
||||
if start_ts <= 0:
|
||||
start_ts = max(0, end_ts - (14 * 24 * 60 * 60 * 1000))
|
||||
|
||||
events_qs = ContactAvailabilityEvent.objects.filter(user=request.user)
|
||||
spans_qs = ContactAvailabilitySpan.objects.filter(user=request.user)
|
||||
|
||||
if person_id:
|
||||
events_qs = events_qs.filter(person_id=person_id)
|
||||
spans_qs = spans_qs.filter(person_id=person_id)
|
||||
if service:
|
||||
events_qs = events_qs.filter(service=service)
|
||||
spans_qs = spans_qs.filter(service=service)
|
||||
if state:
|
||||
events_qs = events_qs.filter(availability_state=state)
|
||||
spans_qs = spans_qs.filter(state=state)
|
||||
if source_kind:
|
||||
events_qs = events_qs.filter(source_kind=source_kind)
|
||||
|
||||
events_qs = events_qs.filter(ts__gte=start_ts, ts__lte=end_ts)
|
||||
spans_qs = spans_qs.filter(start_ts__lte=end_ts, end_ts__gte=start_ts)
|
||||
|
||||
events = list(
|
||||
events_qs.select_related("person", "person_identifier").order_by("-ts")[:500]
|
||||
)
|
||||
spans = list(
|
||||
spans_qs.select_related("person", "person_identifier").order_by("-end_ts")[:500]
|
||||
)
|
||||
|
||||
people = list(Person.objects.filter(user=request.user).order_by("name"))
|
||||
|
||||
context = {
|
||||
"settings_row": settings_row,
|
||||
"people": people,
|
||||
"events": events,
|
||||
"spans": spans,
|
||||
"filters": {
|
||||
"person": person_id,
|
||||
"service": service,
|
||||
"state": state,
|
||||
"source_kind": source_kind,
|
||||
"start": request.GET.get("start") or "",
|
||||
"end": request.GET.get("end") or "",
|
||||
},
|
||||
"service_choices": ["signal", "whatsapp", "xmpp", "instagram", "web"],
|
||||
"state_choices": ["available", "fading", "unavailable", "unknown"],
|
||||
"source_kind_choices": [
|
||||
"native_presence",
|
||||
"read_receipt",
|
||||
"typing_start",
|
||||
"typing_stop",
|
||||
"message_in",
|
||||
"message_out",
|
||||
"inferred_timeout",
|
||||
],
|
||||
}
|
||||
return render(request, self.template_name, context)
|
||||
@@ -48,6 +48,8 @@ from core.models import (
|
||||
PlatformChatLink,
|
||||
WorkspaceConversation,
|
||||
)
|
||||
from core.presence import get_settings as get_availability_settings
|
||||
from core.presence import spans_for_range
|
||||
from core.realtime.typing_state import get_person_typing_state
|
||||
from core.translation.engine import process_inbound_translation
|
||||
from core.views.workspace import (
|
||||
@@ -101,6 +103,36 @@ def _default_service(service: str | None) -> str:
|
||||
return "signal"
|
||||
|
||||
|
||||
def _identifier_variants(service: str, identifier: str) -> list[str]:
|
||||
value = str(identifier or "").strip()
|
||||
if not value:
|
||||
return []
|
||||
service_key = _default_service(service)
|
||||
variants = [value]
|
||||
|
||||
bare = value.split("@", 1)[0].strip()
|
||||
if bare and bare not in variants:
|
||||
variants.append(bare)
|
||||
|
||||
if service_key == "signal":
|
||||
digits = re.sub(r"[^0-9]", "", value)
|
||||
if digits and digits not in variants:
|
||||
variants.append(digits)
|
||||
if digits:
|
||||
plus = f"+{digits}"
|
||||
if plus not in variants:
|
||||
variants.append(plus)
|
||||
elif service_key == "whatsapp":
|
||||
if bare:
|
||||
direct = f"{bare}@s.whatsapp.net"
|
||||
group = f"{bare}@g.us"
|
||||
if direct not in variants:
|
||||
variants.append(direct)
|
||||
if group not in variants:
|
||||
variants.append(group)
|
||||
return variants
|
||||
|
||||
|
||||
def _safe_limit(raw) -> int:
|
||||
try:
|
||||
value = int(raw or 40)
|
||||
@@ -136,6 +168,24 @@ def _format_ts_label(ts_value: int) -> str:
|
||||
return str(ts_value or "")
|
||||
|
||||
|
||||
def _serialize_availability_spans(spans):
|
||||
rows = []
|
||||
for row in list(spans or []):
|
||||
rows.append(
|
||||
{
|
||||
"id": int(getattr(row, "id", 0) or 0),
|
||||
"service": str(getattr(row, "service", "") or ""),
|
||||
"state": str(getattr(row, "state", "unknown") or "unknown"),
|
||||
"start_ts": int(getattr(row, "start_ts", 0) or 0),
|
||||
"end_ts": int(getattr(row, "end_ts", 0) or 0),
|
||||
"confidence_start": float(getattr(row, "confidence_start", 0.0) or 0.0),
|
||||
"confidence_end": float(getattr(row, "confidence_end", 0.0) or 0.0),
|
||||
"payload": dict(getattr(row, "payload", {}) or {}),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _is_outgoing(msg: Message) -> bool:
|
||||
is_outgoing = str(msg.custom_author or "").upper() in {"USER", "BOT"}
|
||||
if not is_outgoing:
|
||||
@@ -672,14 +722,19 @@ THREAD_METRIC_COPY_OVERRIDES = {
|
||||
def _workspace_conversation_for_person(user, person):
|
||||
if person is None:
|
||||
return None
|
||||
return (
|
||||
WorkspaceConversation.objects.filter(
|
||||
user=user,
|
||||
participants=person,
|
||||
try:
|
||||
from core.views.workspace import _conversation_for_person
|
||||
|
||||
return _conversation_for_person(user, person)
|
||||
except Exception:
|
||||
return (
|
||||
WorkspaceConversation.objects.filter(
|
||||
user=user,
|
||||
participants=person,
|
||||
)
|
||||
.order_by("-last_event_ts", "-created_at")
|
||||
.first()
|
||||
)
|
||||
.order_by("-last_event_ts", "-created_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def _counterpart_identifiers_for_person(user, person):
|
||||
@@ -1522,14 +1577,15 @@ def _engage_source_from_ref(plan, source_ref):
|
||||
|
||||
|
||||
def _context_base(user, service, identifier, person):
|
||||
identifier_variants = _identifier_variants(service, identifier)
|
||||
person_identifier = None
|
||||
if person is not None:
|
||||
if identifier:
|
||||
if identifier_variants:
|
||||
person_identifier = PersonIdentifier.objects.filter(
|
||||
user=user,
|
||||
person=person,
|
||||
service=service,
|
||||
identifier=identifier,
|
||||
identifier__in=identifier_variants,
|
||||
).first()
|
||||
if person_identifier is None:
|
||||
person_identifier = (
|
||||
@@ -1544,7 +1600,7 @@ def _context_base(user, service, identifier, person):
|
||||
person_identifier = PersonIdentifier.objects.filter(
|
||||
user=user,
|
||||
service=service,
|
||||
identifier=identifier,
|
||||
identifier__in=identifier_variants or [identifier],
|
||||
).first()
|
||||
|
||||
if person_identifier:
|
||||
@@ -2496,6 +2552,35 @@ def _panel_context(
|
||||
counterpart_identifiers=counterpart_identifiers,
|
||||
conversation=conversation,
|
||||
)
|
||||
availability_slices = []
|
||||
availability_enabled = False
|
||||
availability_settings = get_availability_settings(request.user)
|
||||
if (
|
||||
base["person"] is not None
|
||||
and availability_settings.enabled
|
||||
and availability_settings.show_in_chat
|
||||
):
|
||||
range_start = (
|
||||
int(session_bundle["messages"][0].ts or 0) if session_bundle["messages"] else 0
|
||||
)
|
||||
range_end = (
|
||||
int(session_bundle["messages"][-1].ts or 0) if session_bundle["messages"] else 0
|
||||
)
|
||||
if range_start <= 0 or range_end <= 0:
|
||||
now_ts = int(time.time() * 1000)
|
||||
range_start = now_ts - (24 * 60 * 60 * 1000)
|
||||
range_end = now_ts
|
||||
availability_enabled = True
|
||||
availability_slices = _serialize_availability_spans(
|
||||
spans_for_range(
|
||||
user=request.user,
|
||||
person=base["person"],
|
||||
start_ts=range_start,
|
||||
end_ts=range_end,
|
||||
service=base["service"],
|
||||
limit=200,
|
||||
)
|
||||
)
|
||||
glance_items = _build_glance_items(
|
||||
serialized_messages,
|
||||
person_id=(base["person"].id if base["person"] else None),
|
||||
@@ -2665,6 +2750,22 @@ def _panel_context(
|
||||
if base["person"]
|
||||
else reverse("ai_workspace")
|
||||
),
|
||||
"ai_workspace_graphs_url": (
|
||||
reverse(
|
||||
"ai_workspace_insight_graphs",
|
||||
kwargs={"type": "page", "person_id": base["person"].id},
|
||||
)
|
||||
if base["person"]
|
||||
else ""
|
||||
),
|
||||
"ai_workspace_info_url": (
|
||||
reverse(
|
||||
"ai_workspace_information",
|
||||
kwargs={"type": "page", "person_id": base["person"].id},
|
||||
)
|
||||
if base["person"]
|
||||
else ""
|
||||
),
|
||||
"ai_workspace_widget_url": (
|
||||
(
|
||||
f"{reverse('ai_workspace_person', kwargs={'type': 'widget', 'person_id': base['person'].id})}"
|
||||
@@ -2676,6 +2777,9 @@ def _panel_context(
|
||||
"manual_icon_class": "fa-solid fa-paper-plane",
|
||||
"panel_id": f"compose-panel-{unique}",
|
||||
"typing_state_json": json.dumps(typing_state),
|
||||
"availability_enabled": availability_enabled,
|
||||
"availability_slices": availability_slices,
|
||||
"availability_slices_json": json.dumps(availability_slices),
|
||||
"command_options": command_options,
|
||||
"bp_binding_summary": bp_binding_summary,
|
||||
"platform_options": platform_options,
|
||||
@@ -3133,6 +3237,31 @@ class ComposeThread(LoginRequiredMixin, View):
|
||||
counterpart_identifiers = _counterpart_identifiers_for_person(
|
||||
request.user, base["person"]
|
||||
)
|
||||
availability_slices = []
|
||||
availability_settings = get_availability_settings(request.user)
|
||||
if (
|
||||
base["person"] is not None
|
||||
and availability_settings.enabled
|
||||
and availability_settings.show_in_chat
|
||||
):
|
||||
range_start = (
|
||||
int(messages[0].ts or 0) if messages else max(0, int(after_ts or 0))
|
||||
)
|
||||
range_end = int(latest_ts or 0)
|
||||
if range_start <= 0 or range_end <= 0:
|
||||
now_ts = int(time.time() * 1000)
|
||||
range_start = now_ts - (24 * 60 * 60 * 1000)
|
||||
range_end = now_ts
|
||||
availability_slices = _serialize_availability_spans(
|
||||
spans_for_range(
|
||||
user=request.user,
|
||||
person=base["person"],
|
||||
start_ts=range_start,
|
||||
end_ts=range_end,
|
||||
service=base["service"],
|
||||
limit=200,
|
||||
)
|
||||
)
|
||||
payload = {
|
||||
"messages": _serialize_messages_with_artifacts(
|
||||
messages,
|
||||
@@ -3141,6 +3270,7 @@ class ComposeThread(LoginRequiredMixin, View):
|
||||
seed_previous=seed_previous,
|
||||
),
|
||||
"last_ts": latest_ts,
|
||||
"availability_slices": availability_slices,
|
||||
"typing": get_person_typing_state(
|
||||
user_id=request.user.id,
|
||||
person_id=base["person"].id if base["person"] else None,
|
||||
|
||||
@@ -12,6 +12,8 @@ from mixins.views import ObjectList, ObjectRead
|
||||
|
||||
from core.clients import transport
|
||||
from core.models import Chat, PersonIdentifier, PlatformChatLink
|
||||
from core.presence import get_settings as get_availability_settings
|
||||
from core.presence import latest_state_for_people
|
||||
from core.views.manage.permissions import SuperUserRequiredMixin
|
||||
|
||||
|
||||
@@ -211,6 +213,10 @@ class SignalChatsList(SuperUserRequiredMixin, ObjectList):
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
pk = self.kwargs.get("pk", "")
|
||||
availability_settings = get_availability_settings(self.request.user)
|
||||
show_availability = bool(
|
||||
availability_settings.enabled and availability_settings.show_in_groups
|
||||
)
|
||||
chats = list(
|
||||
Chat.objects.filter(
|
||||
Q(account=pk) | Q(account__isnull=True) | Q(account="")
|
||||
@@ -265,6 +271,9 @@ class SignalChatsList(SuperUserRequiredMixin, ObjectList):
|
||||
"person_name": (
|
||||
person_identifier.person.name if person_identifier else ""
|
||||
),
|
||||
"person_id": (
|
||||
str(person_identifier.person_id) if person_identifier else ""
|
||||
),
|
||||
"manual_icon_class": "fa-solid fa-paper-plane",
|
||||
"can_compose": bool(compose_page_url),
|
||||
"match_url": (
|
||||
@@ -300,6 +309,56 @@ class SignalChatsList(SuperUserRequiredMixin, ObjectList):
|
||||
}
|
||||
)
|
||||
|
||||
if show_availability:
|
||||
person_ids = [
|
||||
str(item.get("person_id") or "").strip()
|
||||
for item in rows
|
||||
if str(item.get("person_id") or "").strip()
|
||||
]
|
||||
person_ids = [pid for pid in person_ids if pid]
|
||||
state_map = latest_state_for_people(
|
||||
user=self.request.user,
|
||||
person_ids=person_ids,
|
||||
service="signal",
|
||||
)
|
||||
for row in rows:
|
||||
pid = str(row.get("person_id") or "").strip()
|
||||
if pid and pid in state_map:
|
||||
state_row = state_map.get(pid) or {}
|
||||
row["availability_state"] = str(state_row.get("state") or "unknown")
|
||||
row["availability_label"] = (
|
||||
f"{str(state_row.get('state') or 'unknown').title()} "
|
||||
f"({float(state_row.get('confidence') or 0.0):.2f})"
|
||||
)
|
||||
signal_person_ids = list(
|
||||
PersonIdentifier.objects.filter(
|
||||
user=self.request.user,
|
||||
service="signal",
|
||||
)
|
||||
.exclude(person_id__isnull=True)
|
||||
.values_list("person_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
group_states = latest_state_for_people(
|
||||
user=self.request.user,
|
||||
person_ids=[str(pid) for pid in signal_person_ids if str(pid)],
|
||||
service="signal",
|
||||
)
|
||||
aggregate_counts = {"available": 0, "fading": 0}
|
||||
for state_row in group_states.values():
|
||||
state_text = str((state_row or {}).get("state") or "").strip().lower()
|
||||
if state_text in aggregate_counts:
|
||||
aggregate_counts[state_text] += 1
|
||||
aggregate_label = (
|
||||
f"{aggregate_counts['available']} available · {aggregate_counts['fading']} fading"
|
||||
if (aggregate_counts["available"] or aggregate_counts["fading"])
|
||||
else ""
|
||||
)
|
||||
if aggregate_label:
|
||||
for row in rows:
|
||||
if row.get("is_group"):
|
||||
row["availability_label"] = aggregate_label
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ from core.models import (
|
||||
PersonIdentifier,
|
||||
PlatformChatLink,
|
||||
Chat,
|
||||
ExternalChatLink,
|
||||
)
|
||||
from core.tasks.providers.mock import get_provider
|
||||
from core.tasks.providers import get_provider
|
||||
|
||||
SAFE_TASK_FLAGS_DEFAULTS = {
|
||||
"derive_enabled": True,
|
||||
@@ -170,6 +171,13 @@ def _service_label(service: str) -> str:
|
||||
return labels.get(key, key.title() if key else "Unknown")
|
||||
|
||||
|
||||
def _provider_row_map(user):
|
||||
return {
|
||||
str(row.provider or "").strip().lower(): row
|
||||
for row in TaskProviderConfig.objects.filter(user=user).order_by("provider")
|
||||
}
|
||||
|
||||
|
||||
def _resolve_channel_display(user, service: str, identifier: str) -> dict:
|
||||
service_key = str(service or "").strip().lower()
|
||||
raw_identifier = str(identifier or "").strip()
|
||||
@@ -408,12 +416,33 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
for row in sources:
|
||||
row.settings_effective = _flags_with_defaults(row.settings)
|
||||
row.allowed_prefixes_csv = ",".join(row.settings_effective["allowed_prefixes"])
|
||||
provider_map = _provider_row_map(request.user)
|
||||
codex_cfg = provider_map.get("codex_cli")
|
||||
codex_settings = dict(getattr(codex_cfg, "settings", {}) or {})
|
||||
mock_cfg = provider_map.get("mock")
|
||||
external_chat_links = list(
|
||||
ExternalChatLink.objects.filter(user=request.user).select_related(
|
||||
"person", "person_identifier"
|
||||
).order_by("-updated_at")[:200]
|
||||
)
|
||||
|
||||
return {
|
||||
"projects": projects,
|
||||
"epics": TaskEpic.objects.filter(project__user=request.user).select_related("project").order_by("project__name", "name"),
|
||||
"sources": sources,
|
||||
"patterns": TaskCompletionPattern.objects.filter(user=request.user).order_by("position", "created_at"),
|
||||
"provider_configs": TaskProviderConfig.objects.filter(user=request.user).order_by("provider"),
|
||||
"provider_configs": list(provider_map.values()),
|
||||
"mock_provider_config": mock_cfg,
|
||||
"codex_provider_config": codex_cfg,
|
||||
"codex_provider_settings": {
|
||||
"command": str(codex_settings.get("command") or "codex"),
|
||||
"workspace_root": str(codex_settings.get("workspace_root") or ""),
|
||||
"default_profile": str(codex_settings.get("default_profile") or ""),
|
||||
"timeout_seconds": int(codex_settings.get("timeout_seconds") or 60),
|
||||
"chat_link_mode": str(codex_settings.get("chat_link_mode") or "task-sync"),
|
||||
},
|
||||
"person_identifiers": PersonIdentifier.objects.filter(user=request.user).select_related("person").order_by("person__name", "service", "identifier")[:600],
|
||||
"external_chat_links": external_chat_links,
|
||||
"sync_events": ExternalSyncEvent.objects.filter(user=request.user).order_by("-updated_at")[:100],
|
||||
"prefill_service": prefill_service,
|
||||
"prefill_identifier": prefill_identifier,
|
||||
@@ -537,18 +566,81 @@ class TaskSettings(LoginRequiredMixin, View):
|
||||
defaults={"enabled": False, "settings": {}},
|
||||
)
|
||||
row.enabled = bool(request.POST.get("enabled"))
|
||||
row.save(update_fields=["enabled", "updated_at"])
|
||||
settings_payload = dict(row.settings or {})
|
||||
if provider == "codex_cli":
|
||||
timeout_raw = str(request.POST.get("timeout_seconds") or "60").strip()
|
||||
try:
|
||||
timeout_value = max(1, int(timeout_raw))
|
||||
except Exception:
|
||||
timeout_value = 60
|
||||
settings_payload = {
|
||||
"command": str(request.POST.get("command") or "codex").strip() or "codex",
|
||||
"workspace_root": str(request.POST.get("workspace_root") or "").strip(),
|
||||
"default_profile": str(request.POST.get("default_profile") or "").strip(),
|
||||
"timeout_seconds": timeout_value,
|
||||
"chat_link_mode": "task-sync",
|
||||
}
|
||||
row.settings = settings_payload
|
||||
row.save(update_fields=["enabled", "settings", "updated_at"])
|
||||
return _settings_redirect(request)
|
||||
|
||||
if action == "external_chat_link_upsert":
|
||||
provider = str(request.POST.get("provider") or "codex_cli").strip().lower() or "codex_cli"
|
||||
external_chat_id = str(request.POST.get("external_chat_id") or "").strip()
|
||||
person_identifier_id = str(request.POST.get("person_identifier_id") or "").strip()
|
||||
if not external_chat_id:
|
||||
messages.error(request, "External chat ID is required.")
|
||||
return _settings_redirect(request)
|
||||
identifier = None
|
||||
if person_identifier_id:
|
||||
identifier = get_object_or_404(
|
||||
PersonIdentifier,
|
||||
user=request.user,
|
||||
id=person_identifier_id,
|
||||
)
|
||||
row, _ = ExternalChatLink.objects.update_or_create(
|
||||
user=request.user,
|
||||
provider=provider,
|
||||
external_chat_id=external_chat_id,
|
||||
defaults={
|
||||
"person": getattr(identifier, "person", None),
|
||||
"person_identifier": identifier,
|
||||
"enabled": bool(request.POST.get("enabled")),
|
||||
"metadata": {
|
||||
"chat_link_mode": "task-sync",
|
||||
"notes": str(request.POST.get("metadata_notes") or "").strip(),
|
||||
},
|
||||
},
|
||||
)
|
||||
if identifier and row.person_id != identifier.person_id:
|
||||
row.person = identifier.person
|
||||
row.save(update_fields=["person", "updated_at"])
|
||||
return _settings_redirect(request)
|
||||
|
||||
if action == "external_chat_link_delete":
|
||||
row = get_object_or_404(
|
||||
ExternalChatLink,
|
||||
id=request.POST.get("external_link_id"),
|
||||
user=request.user,
|
||||
)
|
||||
row.delete()
|
||||
return _settings_redirect(request)
|
||||
|
||||
if action == "sync_retry":
|
||||
event = get_object_or_404(ExternalSyncEvent, id=request.POST.get("event_id"), user=request.user)
|
||||
provider = get_provider(event.provider)
|
||||
payload = dict(event.payload or {})
|
||||
result = provider.append_update({}, payload)
|
||||
event.status = "ok" if result.ok else "failed"
|
||||
event.error = str(result.error or "")
|
||||
event.payload = dict(payload, retried=True)
|
||||
event.save(update_fields=["status", "error", "payload", "updated_at"])
|
||||
if bool(getattr(provider, "run_in_worker", False)):
|
||||
event.status = "pending"
|
||||
event.error = ""
|
||||
event.payload = dict(event.payload or {}, retried=True)
|
||||
event.save(update_fields=["status", "error", "payload", "updated_at"])
|
||||
else:
|
||||
payload = dict(event.payload or {})
|
||||
result = provider.append_update({}, payload)
|
||||
event.status = "ok" if result.ok else "failed"
|
||||
event.error = str(result.error or "")
|
||||
event.payload = dict(payload, retried=True)
|
||||
event.save(update_fields=["status", "error", "payload", "updated_at"])
|
||||
return _settings_redirect(request)
|
||||
|
||||
return _settings_redirect(request)
|
||||
|
||||
@@ -8,6 +8,8 @@ from mixins.views import ObjectList, ObjectRead
|
||||
|
||||
from core.clients import transport
|
||||
from core.models import PersonIdentifier, PlatformChatLink
|
||||
from core.presence import get_settings as get_availability_settings
|
||||
from core.presence import latest_state_for_people
|
||||
from core.util import logs
|
||||
from core.views.compose import _compose_urls, _service_icon_class
|
||||
from core.views.manage.permissions import SuperUserRequiredMixin
|
||||
@@ -265,6 +267,10 @@ class WhatsAppChatsList(WhatsAppContactsList):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
rows = []
|
||||
seen = set()
|
||||
availability_settings = get_availability_settings(self.request.user)
|
||||
show_availability = bool(
|
||||
availability_settings.enabled and availability_settings.show_in_groups
|
||||
)
|
||||
state = transport.get_runtime_state("whatsapp")
|
||||
|
||||
runtime_contacts = state.get("contacts") or []
|
||||
@@ -414,7 +420,36 @@ class WhatsAppChatsList(WhatsAppContactsList):
|
||||
if not row.get("is_group") and row.get("identifier") in db_group_ids:
|
||||
row["is_group"] = True
|
||||
|
||||
return [row for row in rows if row.get("is_group")]
|
||||
group_rows = [row for row in rows if row.get("is_group")]
|
||||
if show_availability and group_rows:
|
||||
whatsapp_person_ids = list(
|
||||
PersonIdentifier.objects.filter(
|
||||
user=self.request.user,
|
||||
service="whatsapp",
|
||||
)
|
||||
.exclude(person_id__isnull=True)
|
||||
.values_list("person_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
state_map = latest_state_for_people(
|
||||
user=self.request.user,
|
||||
person_ids=[str(pid) for pid in whatsapp_person_ids if str(pid)],
|
||||
service="whatsapp",
|
||||
)
|
||||
counts = {"available": 0, "fading": 0}
|
||||
for value in state_map.values():
|
||||
state_text = str((value or {}).get("state") or "").strip().lower()
|
||||
if state_text in counts:
|
||||
counts[state_text] += 1
|
||||
aggregate = (
|
||||
f"{counts['available']} available · {counts['fading']} fading"
|
||||
if (counts["available"] or counts["fading"])
|
||||
else ""
|
||||
)
|
||||
if aggregate:
|
||||
for row in group_rows:
|
||||
row["availability_label"] = aggregate
|
||||
return group_rows
|
||||
|
||||
|
||||
class WhatsAppAccountAdd(SuperUserRequiredMixin, ObjectRead):
|
||||
|
||||
Reference in New Issue
Block a user