Files
GIA/core/tasks/chat_defaults.py
2026-03-08 23:16:15 +00:00

166 lines
5.4 KiB
Python

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,
}
WHATSAPP_GROUP_ID_RE = re.compile(r"^\d+@g\.us$")
WHATSAPP_DIRECT_ID_RE = re.compile(r"^\d+@s\.whatsapp\.net$")
WHATSAPP_BARE_ID_RE = re.compile(r"^\d+$")
SIGNAL_GROUP_ID_RE = re.compile(r"^group\.[A-Za-z0-9+/=]+$")
SIGNAL_UUID_RE = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
re.IGNORECASE,
)
SIGNAL_PHONE_RE = re.compile(r"^\+\d+$")
SIGNAL_INTERNAL_ID_RE = re.compile(r"^[A-Za-z0-9+/=]+$")
def _normalize_whatsapp_identifier(identifier: str) -> str:
value = str(identifier or "").strip()
if not value:
return ""
if "/" in value or "?" in value or "#" in value:
return ""
if WHATSAPP_GROUP_ID_RE.fullmatch(value):
return value
if WHATSAPP_DIRECT_ID_RE.fullmatch(value):
return value
bare = value.split("@", 1)[0].strip()
if not WHATSAPP_BARE_ID_RE.fullmatch(bare):
return ""
if value.endswith("@s.whatsapp.net"):
return f"{bare}@s.whatsapp.net"
return f"{bare}@g.us"
def _normalize_signal_identifier(identifier: str) -> str:
value = str(identifier or "").strip()
if not value:
return ""
if SIGNAL_GROUP_ID_RE.fullmatch(value):
return value
if SIGNAL_UUID_RE.fullmatch(value):
return value.lower()
if SIGNAL_PHONE_RE.fullmatch(value):
return value
if SIGNAL_INTERNAL_ID_RE.fullmatch(value):
return value
return ""
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":
return _normalize_whatsapp_identifier(value)
if service_key == "signal":
return _normalize_signal_identifier(value)
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),
)