from __future__ import annotations import re from core.models import ChatTaskSource, TaskProject from core.tasks.codex_support import channel_variants SAFE_TASK_FLAGS_DEFAULTS = { "derive_enabled": True, "match_mode": "strict", "require_prefix": True, "allowed_prefixes": ["task:", "todo:"], "completion_enabled": True, "ai_title_enabled": True, "announce_task_id": False, "min_chars": 3, } def normalize_channel_identifier(service: str, identifier: str) -> str: service_key = str(service or "").strip().lower() value = str(identifier or "").strip() if not value: return "" if service_key == "whatsapp": bare = value.split("@", 1)[0].strip() if not bare: return value if value.endswith("@s.whatsapp.net"): return f"{bare}@s.whatsapp.net" return f"{bare}@g.us" return value def resolve_message_scope(message) -> tuple[str, str]: source_service = str(getattr(message, "source_service", "") or "").strip().lower() source_channel = str(getattr(message, "source_chat_id", "") or "").strip() if source_service != "web": return source_service, source_channel identifier = getattr(getattr(message, "session", None), "identifier", None) fallback_service = str(getattr(identifier, "service", "") or "").strip().lower() fallback_identifier = str(getattr(identifier, "identifier", "") or "").strip() if fallback_service and fallback_identifier and fallback_service != "web": return fallback_service, fallback_identifier return source_service, source_channel def _project_name_candidate(service: str, channel_identifier: str, message=None) -> str: person_name = "" if message is not None: identifier = getattr(getattr(message, "session", None), "identifier", None) person = getattr(identifier, "person", None) person_name = str(getattr(person, "name", "") or "").strip() if person_name: return person_name[:255] raw = str(channel_identifier or "").strip() if str(service or "").strip().lower() == "whatsapp": raw = raw.split("@", 1)[0].strip() cleaned = re.sub(r"\s+", " ", raw).strip() if not cleaned: cleaned = "Chat" return f"Chat: {cleaned}"[:255] def _ensure_unique_project_name(user, base_name: str) -> str: base = str(base_name or "").strip() or "Chat" if not TaskProject.objects.filter(user=user, name=base).exists(): return base idx = 2 while idx < 10000: candidate = f"{base} ({idx})"[:255] if not TaskProject.objects.filter(user=user, name=candidate).exists(): return candidate idx += 1 return f"{base} ({str(user.id)[:8]})"[:255] def ensure_default_source_for_chat( *, user, service: str, channel_identifier: str, message=None, ): service_key = str(service or "").strip().lower() normalized_identifier = normalize_channel_identifier(service_key, channel_identifier) variants = channel_variants(service_key, normalized_identifier) if not service_key or not variants: return None existing = ( ChatTaskSource.objects.filter( user=user, service=service_key, channel_identifier__in=variants, ) .select_related("project", "epic") .order_by("-enabled", "-updated_at", "-created_at") .first() ) if existing is not None: if not existing.enabled: existing.enabled = True existing.save(update_fields=["enabled", "updated_at"]) return existing project_name = _ensure_unique_project_name( user, _project_name_candidate(service_key, normalized_identifier, message=message), ) project = TaskProject.objects.create( user=user, name=project_name, settings=dict(SAFE_TASK_FLAGS_DEFAULTS), ) return ChatTaskSource.objects.create( user=user, service=service_key, channel_identifier=normalized_identifier, project=project, epic=None, enabled=True, settings=dict(SAFE_TASK_FLAGS_DEFAULTS), )