166 lines
5.4 KiB
Python
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),
|
|
)
|