from __future__ import annotations from asgiref.sync import sync_to_async from django.conf import settings from core.commands.base import CommandContext, CommandResult from core.commands.handlers.bp import ( BPCommandHandler, bp_reply_is_optional_for_trigger, bp_trigger_matches, ) from core.commands.registry import get as get_handler from core.commands.registry import register from core.messaging.reply_sync import is_mirrored_origin from core.models import CommandChannelBinding, CommandProfile, Message from core.util import logs log = logs.get_logger("command_engine") _REGISTERED = False def _channel_variants(service: str, channel_identifier: str) -> list[str]: value = str(channel_identifier or "").strip() if not value: return [] variants = [value] svc = str(service or "").strip().lower() if svc == "whatsapp": bare = value.split("@", 1)[0].strip() if bare and bare not in variants: variants.append(bare) group = f"{bare}@g.us" if bare else "" if group and group not in variants: variants.append(group) return variants def ensure_handlers_registered(): global _REGISTERED if _REGISTERED: return register(BPCommandHandler()) _REGISTERED = True async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]: def _load(): direct_variants = _channel_variants(ctx.service, ctx.channel_identifier) if not direct_variants: return [] direct = list( CommandProfile.objects.filter( user_id=ctx.user_id, enabled=True, channel_bindings__enabled=True, channel_bindings__direction="ingress", channel_bindings__service=ctx.service, channel_bindings__channel_identifier__in=direct_variants, ).distinct() ) if direct: return direct # Compose-originated messages use `web` service even when the # underlying conversation is mapped to a platform identifier. if str(ctx.service or "").strip().lower() != "web": return [] trigger = ( Message.objects.select_related("session", "session__identifier") .filter(id=ctx.message_id, user_id=ctx.user_id) .first() ) identifier = getattr(getattr(trigger, "session", None), "identifier", None) fallback_service = str(getattr(identifier, "service", "") or "").strip().lower() fallback_identifier = str(getattr(identifier, "identifier", "") or "").strip() fallback_variants = _channel_variants(fallback_service, fallback_identifier) if not fallback_service or not fallback_variants: return [] return list( CommandProfile.objects.filter( user_id=ctx.user_id, enabled=True, channel_bindings__enabled=True, channel_bindings__direction="ingress", channel_bindings__service=fallback_service, channel_bindings__channel_identifier__in=fallback_variants, ).distinct() ) return await sync_to_async(_load)() def _matches_trigger(profile: CommandProfile, text: str) -> bool: if profile.slug == "bp" and bool(getattr(settings, "BP_SUBCOMMANDS_V1", True)): return bp_trigger_matches( message_text=text, trigger_token=profile.trigger_token, exact_match_only=profile.exact_match_only, ) body = str(text or "").strip() trigger = str(profile.trigger_token or "").strip() if not trigger: return False if profile.exact_match_only: return body == trigger return trigger in body async def process_inbound_message(ctx: CommandContext) -> list[CommandResult]: ensure_handlers_registered() trigger_message = await sync_to_async( lambda: Message.objects.filter(id=ctx.message_id).first() )() if trigger_message is None: return [] if is_mirrored_origin(trigger_message.message_meta): return [] profiles = await _eligible_profiles(ctx) results: list[CommandResult] = [] for profile in profiles: if not _matches_trigger(profile, ctx.message_text): continue if profile.reply_required and trigger_message.reply_to_id is None: if ( profile.slug == "bp" and bool(getattr(settings, "BP_SUBCOMMANDS_V1", True)) and bp_reply_is_optional_for_trigger(ctx.message_text) ): pass else: results.append( CommandResult( ok=False, status="skipped", error="reply_required", payload={"profile": profile.slug}, ) ) continue handler = get_handler(profile.slug) if handler is None: results.append( CommandResult( ok=False, status="failed", error=f"missing_handler:{profile.slug}", ) ) continue try: result = await handler.execute(ctx) results.append(result) except Exception as exc: log.exception("command execution failed for profile=%s: %s", profile.slug, exc) results.append( CommandResult( ok=False, status="failed", error=f"handler_exception:{exc}", ) ) return results